blob: e8b2fe7fa37275885f3d497012e49f56daf23fb8 [file] [log] [blame]
// Copyright 2021 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 {
crate::{
client::{bss_selection, network_selection, types},
config_management::{self, PastConnectionData, SavedNetworksManagerApi},
telemetry::{DisconnectInfo, TelemetryEvent, TelemetrySender},
util::{
listener::{
ClientListenerMessageSender, ClientNetworkState, ClientStateUpdate,
Message::NotifyListeners,
},
state_machine::{self, ExitReason, IntoStateExt},
},
},
anyhow::format_err,
fidl::endpoints::create_proxy,
fidl_fuchsia_wlan_common as fidl_common, fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211,
fidl_fuchsia_wlan_internal as fidl_internal, fidl_fuchsia_wlan_policy as fidl_policy,
fidl_fuchsia_wlan_sme as fidl_sme,
fuchsia_async::{self as fasync, DurationExt},
fuchsia_cobalt::CobaltSender,
fuchsia_zircon as zx,
futures::{
channel::{mpsc, oneshot},
future::{self, FutureExt},
select,
stream::{self, FuturesUnordered, StreamExt, TryStreamExt},
},
log::{debug, error, info},
std::{convert::TryFrom, sync::Arc},
void::ResultVoidErrExt,
wlan_common::{bss::BssDescription, energy::DecibelMilliWatt, stats::SignalStrengthAverage},
wlan_metrics_registry::{
POLICY_CONNECTION_ATTEMPT_METRIC_ID as CONNECTION_ATTEMPT_METRIC_ID,
POLICY_DISCONNECTION_METRIC_ID as DISCONNECTION_METRIC_ID,
},
};
const MAX_CONNECTION_ATTEMPTS: u8 = 4; // arbitrarily chosen until we have some data
type State = state_machine::State<ExitReason>;
type ReqStream = stream::Fuse<mpsc::Receiver<ManualRequest>>;
pub trait ClientApi {
fn connect(
&mut self,
request: types::ConnectRequest,
responder: oneshot::Sender<()>,
) -> Result<(), anyhow::Error>;
fn disconnect(
&mut self,
reason: types::DisconnectReason,
responder: oneshot::Sender<()>,
) -> Result<(), anyhow::Error>;
/// Queries the liveness of the channel used to control the client state machine. If the
/// channel is not alive, this indicates that the client state machine has exited.
fn is_alive(&self) -> bool;
}
pub struct Client {
req_sender: mpsc::Sender<ManualRequest>,
}
impl Client {
pub fn new(req_sender: mpsc::Sender<ManualRequest>) -> Self {
Self { req_sender }
}
}
impl ClientApi for Client {
fn connect(
&mut self,
request: types::ConnectRequest,
responder: oneshot::Sender<()>,
) -> Result<(), anyhow::Error> {
self.req_sender
.try_send(ManualRequest::Connect((request, responder)))
.map_err(|e| format_err!("failed to send connect request: {:?}", e))
}
fn disconnect(
&mut self,
reason: types::DisconnectReason,
responder: oneshot::Sender<()>,
) -> Result<(), anyhow::Error> {
self.req_sender
.try_send(ManualRequest::Disconnect((reason, responder)))
.map_err(|e| format_err!("failed to send disconnect request: {:?}", e))
}
fn is_alive(&self) -> bool {
!self.req_sender.is_closed()
}
}
pub enum ManualRequest {
Connect((types::ConnectRequest, oneshot::Sender<()>)),
Disconnect((types::DisconnectReason, oneshot::Sender<()>)),
}
fn send_listener_state_update(
sender: &ClientListenerMessageSender,
network_update: Option<ClientNetworkState>,
) {
let mut networks = vec![];
if let Some(network) = network_update {
networks.push(network)
}
let updates =
ClientStateUpdate { state: fidl_policy::WlanClientState::ConnectionsEnabled, networks };
match sender.clone().unbounded_send(NotifyListeners(updates)) {
Ok(_) => (),
Err(e) => error!("failed to send state update: {:?}", e),
};
}
pub async fn serve(
iface_id: u16,
has_wpa3_support: bool,
proxy: fidl_sme::ClientSmeProxy,
sme_event_stream: fidl_sme::ClientSmeEventStream,
req_stream: mpsc::Receiver<ManualRequest>,
update_sender: ClientListenerMessageSender,
saved_networks_manager: Arc<dyn SavedNetworksManagerApi>,
connect_request: Option<(types::ConnectRequest, oneshot::Sender<()>)>,
network_selector: Arc<network_selection::NetworkSelector>,
cobalt_api: CobaltSender,
telemetry_sender: TelemetrySender,
stats_sender: ConnectionStatsSender,
) {
let next_network = match connect_request {
Some((req, sender)) => Some(ConnectingOptions {
connect_responder: Some(sender),
connect_request: req,
attempt_counter: 0,
}),
None => None,
};
let disconnect_options = DisconnectingOptions {
disconnect_responder: None,
previous_network: None,
next_network,
reason: types::DisconnectReason::Startup,
};
let common_options = CommonStateOptions {
proxy: proxy,
req_stream: req_stream.fuse(),
update_sender: update_sender,
saved_networks_manager: saved_networks_manager,
network_selector,
cobalt_api,
telemetry_sender,
iface_id,
has_wpa3_support,
stats_sender,
};
let state_machine =
disconnecting_state(common_options, disconnect_options).into_state_machine();
let removal_watcher = sme_event_stream.map_ok(|_| ()).try_collect::<()>();
select! {
state_machine = state_machine.fuse() => {
match state_machine.void_unwrap_err() {
ExitReason(Err(e)) => error!("Client state machine for iface #{} terminated with an error: {:?}",
iface_id, e),
ExitReason(Ok(_)) => info!("Client state machine for iface #{} exited gracefully",
iface_id,),
}
}
removal_watcher = removal_watcher.fuse() => if let Err(e) = removal_watcher {
info!("Error reading from Client SME channel of iface #{}: {:?}",
iface_id, e);
},
}
}
/// Common parameters passed to all states
struct CommonStateOptions {
proxy: fidl_sme::ClientSmeProxy,
req_stream: ReqStream,
update_sender: ClientListenerMessageSender,
saved_networks_manager: Arc<dyn SavedNetworksManagerApi>,
network_selector: Arc<network_selection::NetworkSelector>,
cobalt_api: CobaltSender,
telemetry_sender: TelemetrySender,
iface_id: u16,
has_wpa3_support: bool,
/// Used to send periodic connection stats used to determine whether or not to roam.
stats_sender: mpsc::UnboundedSender<PeriodicConnectionStats>,
}
/// Data that is periodically gathered for determining whether to roam
pub struct PeriodicConnectionStats {
/// ID and BSSID of the current connection, to exclude it when comparing available networks.
pub id: types::NetworkIdentifier,
/// Iface ID that the connection is on.
pub iface_id: u16,
pub quality_data: bss_selection::BssQualityData,
}
pub type ConnectionStatsSender = mpsc::UnboundedSender<PeriodicConnectionStats>;
pub type ConnectionStatsReceiver = mpsc::UnboundedReceiver<PeriodicConnectionStats>;
fn handle_none_request() -> Result<State, ExitReason> {
return Err(ExitReason(Err(format_err!("The stream of requests ended unexpectedly"))));
}
// These functions were introduced to resolve the following error:
// ```
// error[E0391]: cycle detected when evaluating trait selection obligation
// `impl core::future::future::Future: std::marker::Send`
// ```
// which occurs when two functions that return an `impl Trait` call each other
// in a cycle. (e.g. this case `connecting_state` calling `disconnecting_state`,
// which calls `connecting_state`)
fn to_disconnecting_state(
common_options: CommonStateOptions,
disconnecting_options: DisconnectingOptions,
) -> State {
disconnecting_state(common_options, disconnecting_options).into_state()
}
fn to_connecting_state(
common_options: CommonStateOptions,
connecting_options: ConnectingOptions,
) -> State {
connecting_state(common_options, connecting_options).into_state()
}
struct DisconnectingOptions {
disconnect_responder: Option<oneshot::Sender<()>>,
/// Information about the previously connected network, if there was one. Used to send out
/// listener updates.
previous_network: Option<(types::NetworkIdentifier, types::DisconnectStatus)>,
/// Configuration for the next network to connect to, after the disconnect is complete. If not
/// present, the state machine will proceed to IDLE.
next_network: Option<ConnectingOptions>,
reason: types::DisconnectReason,
}
/// The DISCONNECTING state requests an SME disconnect, then transitions to either:
/// - the CONNECTING state if options.next_network is present
/// - exit otherwise
async fn disconnecting_state(
common_options: CommonStateOptions,
options: DisconnectingOptions,
) -> Result<State, ExitReason> {
// Log a message with the disconnect reason
match options.reason {
types::DisconnectReason::FailedToConnect
| types::DisconnectReason::Startup
| types::DisconnectReason::DisconnectDetectedFromSme => {
// These are either just noise or have separate logging, so keep the level at debug.
debug!("Disconnected due to {:?}", options.reason);
}
reason => {
info!("Disconnected due to {:?}", reason);
}
}
// TODO(fxbug.dev/53505): either make this fire-and-forget in the SME, or spawn a thread for this,
// so we don't block on it
common_options
.proxy
.disconnect(types::convert_to_sme_disconnect_reason(options.reason))
.await
.map_err(|e| {
ExitReason(Err(format_err!("Failed to send command to wlanstack: {:?}", e)))
})?;
// Notify listeners of disconnection
let networks = match options.previous_network {
Some((network_identifier, status)) => Some(ClientNetworkState {
id: network_identifier,
state: types::ConnectionState::Disconnected,
status: Some(status),
}),
None => None,
};
send_listener_state_update(&common_options.update_sender, networks);
// Notify the caller that disconnect was sent to the SME once the final disconnected update has
// been sent. This ensures that there will not be a race when the IfaceManager sends out a
// ConnectionsDisabled update.
match options.disconnect_responder {
Some(responder) => responder.send(()).unwrap_or_else(|_| ()),
None => (),
}
// Transition to next state
match options.next_network {
Some(next_network) => Ok(to_connecting_state(common_options, next_network)),
None => Err(ExitReason(Ok(()))),
}
}
fn connect_txn_event_name(event: &fidl_sme::ConnectTransactionEvent) -> &'static str {
match event {
fidl_sme::ConnectTransactionEvent::OnConnectResult { .. } => "OnConnectResult",
fidl_sme::ConnectTransactionEvent::OnDisconnect { .. } => "OnDisconnect",
fidl_sme::ConnectTransactionEvent::OnSignalReport { .. } => "OnSignalReport",
fidl_sme::ConnectTransactionEvent::OnChannelSwitched { .. } => "OnChannelSwitched",
}
}
async fn wait_until_connected(
txn: fidl_sme::ConnectTransactionProxy,
) -> Result<(fidl_sme::ConnectResult, fidl_sme::ConnectTransactionEventStream), anyhow::Error> {
let mut stream = txn.take_event_stream();
if let Some(event) = stream.try_next().await? {
match event {
fidl_sme::ConnectTransactionEvent::OnConnectResult { result } => {
return Ok((result, stream))
}
other => {
return Err(format_err!(
"Expected ConnectTransactionEvent::OnConnectResult, got {}",
connect_txn_event_name(&other)
))
}
}
}
Err(format_err!("Server closed the ConnectTransaction channel before sending a response"))
}
struct ConnectingOptions {
connect_responder: Option<oneshot::Sender<()>>,
connect_request: types::ConnectRequest,
/// Count of previous consecutive failed connection attempts to this same network.
attempt_counter: u8,
}
struct ConnectResult {
sme_result: fidl_sme::ConnectResult,
multiple_bss_candidates: bool,
connect_txn_stream: fidl_sme::ConnectTransactionEventStream,
bss_description: Box<BssDescription>,
}
#[derive(Clone, Debug)]
struct ScanResult {
bss_description: fidl_internal::BssDescription,
has_multiple_bss_candidates: bool,
security_type_detailed: types::SecurityTypeDetailed,
}
impl From<types::ScannedCandidate> for ScanResult {
fn from(candidate: types::ScannedCandidate) -> Self {
let types::ScannedCandidate {
bss_description,
has_multiple_bss_candidates,
security_type_detailed,
..
} = candidate;
ScanResult { bss_description, has_multiple_bss_candidates, security_type_detailed }
}
}
enum SmeOperation {
/// Include information about the time the connection attempt started
ConnectResult(Result<ConnectResult, anyhow::Error>, fasync::Time),
ScanResult(Option<ScanResult>),
}
async fn handle_connecting_error_and_retry(
common_options: CommonStateOptions,
options: ConnectingOptions,
) -> Result<State, ExitReason> {
// Check if the limit for connection attempts to this network has been
// exceeded.
let new_attempt_count = options.attempt_counter + 1;
if new_attempt_count >= MAX_CONNECTION_ATTEMPTS {
info!("Exceeded maximum connection attempts, will not retry");
send_listener_state_update(
&common_options.update_sender,
Some(ClientNetworkState {
id: options.connect_request.target.network,
state: types::ConnectionState::Failed,
status: Some(types::DisconnectStatus::ConnectionFailed),
}),
);
return Err(ExitReason(Ok(())));
} else {
// Limit not exceeded, retry after backing off.
let backoff_time = 400_i64 * i64::from(new_attempt_count);
info!("Will attempt to reconnect after {}ms backoff", backoff_time);
fasync::Timer::new(zx::Duration::from_millis(backoff_time).after_now()).await;
let next_connecting_options = ConnectingOptions {
connect_responder: None,
connect_request: types::ConnectRequest {
reason: types::ConnectReason::RetryAfterFailedConnectAttempt,
..options.connect_request
},
attempt_counter: new_attempt_count,
};
let disconnecting_options = DisconnectingOptions {
disconnect_responder: None,
previous_network: None,
next_network: Some(next_connecting_options),
reason: types::DisconnectReason::FailedToConnect,
};
return Ok(to_disconnecting_state(common_options, disconnecting_options));
}
}
/// The CONNECTING state checks for the required station information in the connection request. If not
/// present, the state first requests an SME scan. For a failed scan, retry connection by passing a
/// next_network to the DISCONNECTING state, as long as there haven't been too many attempts.
/// Next, it requests an SME connect. It handles the SME connect response:
/// - for a successful connection, transition to CONNECTED state
/// - for a failed connection, retry connection by passing a next_network to the
/// DISCONNECTING state, as long as there haven't been too many connection attempts
/// During this time, incoming ManualRequests are also monitored for:
/// - duplicate connect requests are deduped
/// - different connect requests are serviced by passing a next_network to the DISCONNECTING state
/// - disconnect requests cause a transition to DISCONNECTING state
async fn connecting_state<'a>(
mut common_options: CommonStateOptions,
mut options: ConnectingOptions,
) -> Result<State, ExitReason> {
debug!("Entering connecting state");
if options.attempt_counter > 0 {
info!(
"Retrying connection, {} attempts remaining",
MAX_CONNECTION_ATTEMPTS - options.attempt_counter
);
}
// Send a "Connecting" update to listeners, unless this is a retry
if options.attempt_counter == 0 {
send_listener_state_update(
&common_options.update_sender,
Some(ClientNetworkState {
id: options.connect_request.target.network.clone(),
state: types::ConnectionState::Connecting,
status: None,
}),
);
};
// Log a connect attempt in Cobalt
common_options
.cobalt_api
.log_event(CONNECTION_ATTEMPT_METRIC_ID, options.connect_request.reason);
// Let the responder know we've successfully started this connection attempt
match options.connect_responder.take() {
Some(responder) => responder.send(()).unwrap_or_else(|_| ()),
None => {}
}
// If detailed station information was not provided, perform a scan to discover it
let network_selector = common_options.network_selector.clone();
let scan_future = match options.connect_request.target.scanned {
Some(ref scanned) => {
future::ready(SmeOperation::ScanResult(Some(ScanResult::from(scanned.clone())))).boxed()
}
None => {
info!("Connection requested, scanning to find a BSS for the network");
network_selector
.find_connection_candidate_for_network(
common_options.proxy.clone(),
options.connect_request.target.network.clone(),
)
.map(|candidate| {
SmeOperation::ScanResult(candidate.and_then(
|types::ConnectionCandidate { scanned, .. }| scanned.map(ScanResult::from),
))
})
.boxed()
}
};
let mut internal_futures = FuturesUnordered::new();
internal_futures.push(scan_future);
loop {
select! {
// Monitor the SME operations
completed_future = internal_futures.select_next_some() => match completed_future {
SmeOperation::ScanResult(scan) => {
let ScanResult {
bss_description,
has_multiple_bss_candidates: multiple_bss_candidates,
security_type_detailed,
} = match scan {
Some(scan) => scan,
None => {
info!("Failed to find a BSS to which to connect.");
return handle_connecting_error_and_retry(common_options, options).await;
}
};
let parsed_bss_description = BssDescription::try_from(bss_description.clone())
.map_err(|error| {
// This only occurs if an invalid `BssDescription` is received from
// SME, which should never happen.
ExitReason(Err(format_err!(
"Failed to convert BSS description from FIDL: {:?}",
error,
)))
})?;
// TODO(fxbug.dev/102606): Move this call to network selection and write the
// result into a field of `ScannedCandidate`. This code
// should read that field instead of calling this
// function directly.
let authentication = config_management::select_authentication_method(
security_type_detailed,
options.connect_request.target.credential.clone(),
common_options.has_wpa3_support,
)
.ok_or_else(|| {
// This only occurs if invalid or unsupported security criteria are
// received from the network selector, which should never happen.
ExitReason(Err(format_err!(
"Failed to negotiate authentication for {:?} network.",
security_type_detailed,
)))
})?;
// Send a connect request to the SME
let (connect_txn, remote) = create_proxy()
.map_err(|e| ExitReason(Err(format_err!("Failed to create proxy: {:?}", e))))?;
let mut sme_connect_request = fidl_sme::ConnectRequest {
ssid: options.connect_request.target.network.ssid.to_vec(),
bss_description,
multiple_bss_candidates,
authentication,
deprecated_scan_type: fidl_fuchsia_wlan_common::ScanType::Active,
};
common_options.proxy.connect(&mut sme_connect_request, Some(remote)).map_err(|e| {
ExitReason(Err(format_err!("Failed to send command to wlanstack: {:?}", e)))
})?;
let start_time = fasync::Time::now();
let pending_connect_request = wait_until_connected(connect_txn.clone())
.map(move |res| {
let result = res.map(|(sme_result, stream)| ConnectResult {
sme_result,
multiple_bss_candidates,
connect_txn_stream: stream,
bss_description: Box::new(parsed_bss_description),
});
SmeOperation::ConnectResult(result, start_time)
})
.boxed();
internal_futures.push(pending_connect_request);
},
SmeOperation::ConnectResult(connect_result, start_time) => {
let connect_result = connect_result.map_err({
|e| ExitReason(Err(format_err!("failed to send connect to sme: {:?}", e)))
})?;
let sme_result = connect_result.sme_result;
// Notify the saved networks manager of the scan mode with which the network
// was observed if a scan was performed.
let scan_type = options.connect_request.target.scanned
.as_ref()
.and_then(|scanned| match scanned.observation {
types::ScanObservation::Passive => Some(fidl_common::ScanType::Passive),
types::ScanObservation::Active => Some(fidl_common::ScanType::Active),
types::ScanObservation::Unknown => None,
});
common_options.saved_networks_manager.record_connect_result(
options.connect_request.target.network.clone().into(),
&options.connect_request.target.credential,
connect_result.bss_description.bssid,
sme_result,
scan_type
).await;
common_options.telemetry_sender.send(TelemetryEvent::ConnectResult {
latest_ap_state: (*connect_result.bss_description).clone(),
result: sme_result,
multiple_bss_candidates: connect_result.multiple_bss_candidates,
iface_id: common_options.iface_id,
});
match (sme_result.code, sme_result.is_credential_rejected) {
(fidl_ieee80211::StatusCode::Success, _) => {
info!("Successfully connected to network");
send_listener_state_update(
&common_options.update_sender,
Some(ClientNetworkState {
id: options.connect_request.target.network.clone(),
state: types::ConnectionState::Connected,
status: None
}),
);
let connected_options = ConnectedOptions {
currently_fulfilled_request: options.connect_request,
connect_txn_stream: connect_result.connect_txn_stream,
latest_ap_state: connect_result.bss_description,
multiple_bss_candidates: connect_result.multiple_bss_candidates,
connection_attempt_time: start_time,
time_to_connect: fasync::Time::now() - start_time,
};
return Ok(
connected_state(common_options, connected_options).into_state()
);
},
(code, true) => {
info!("Failed to connect: {:?}. Will not retry because of credential error.", code);
send_listener_state_update(
&common_options.update_sender,
Some(ClientNetworkState {
id: options.connect_request.target.network,
state: types::ConnectionState::Failed,
status: Some(types::DisconnectStatus::CredentialsFailed),
}),
);
return Err(ExitReason(Err(format_err!("bad credentials"))));
},
(code, _) => {
info!("Failed to connect: {:?}", code);
return handle_connecting_error_and_retry(common_options, options).await;
}
};
},
},
// Monitor incoming ManualRequests
new_req = common_options.req_stream.next() => match new_req {
Some(ManualRequest::Disconnect((reason, responder))) => {
info!("Cancelling pending connect due to disconnect request");
send_listener_state_update(
&common_options.update_sender,
Some(ClientNetworkState {
id: options.connect_request.target.network,
state: types::ConnectionState::Disconnected,
status: Some(types::DisconnectStatus::ConnectionStopped)
}),
);
let options = DisconnectingOptions {
disconnect_responder: Some(responder),
previous_network: None,
next_network: None,
reason,
};
return Ok(to_disconnecting_state(common_options, options));
}
Some(ManualRequest::Connect((new_connect_request, new_responder))) => {
// Check if it's the same network as we're currently connected to.
// If yes, dedupe the request.
if new_connect_request.target.network == options.connect_request.target.network {
info!("Received duplicate connection request, deduping");
new_responder.send(()).unwrap_or_else(|_| ());
} else {
info!("Cancelling pending connect due to new connection request");
send_listener_state_update(
&common_options.update_sender,
Some(ClientNetworkState {
id: options.connect_request.target.network,
state: types::ConnectionState::Disconnected,
status: Some(types::DisconnectStatus::ConnectionStopped)
}),
);
let next_connecting_options = ConnectingOptions {
connect_responder: Some(new_responder),
connect_request: new_connect_request.clone(),
attempt_counter: 0,
};
let disconnecting_options = DisconnectingOptions {
disconnect_responder: None,
previous_network: None,
next_network: Some(next_connecting_options),
reason: match new_connect_request.reason {
types::ConnectReason::ProactiveNetworkSwitch => types::DisconnectReason::ProactiveNetworkSwitch,
types::ConnectReason::FidlConnectRequest => types::DisconnectReason::FidlConnectRequest,
_ => {
error!("Unexpected connection reason: {:?}", new_connect_request.reason);
types::DisconnectReason::Unknown
}
},
};
return Ok(to_disconnecting_state(common_options, disconnecting_options));
}
}
None => return handle_none_request(),
},
}
}
}
struct ConnectedOptions {
// Keep track of the BSSID we are connected in order to record connection information for
// future network selection.
latest_ap_state: Box<BssDescription>,
multiple_bss_candidates: bool,
currently_fulfilled_request: types::ConnectRequest,
connect_txn_stream: fidl_sme::ConnectTransactionEventStream,
/// Time at which connect was first attempted, historical data for network scoring.
pub connection_attempt_time: fasync::Time,
/// Duration from connection attempt to success, historical data for network scoring.
pub time_to_connect: zx::Duration,
}
/// The CONNECTED state monitors the SME status. It handles the SME status response:
/// - if still connected to the correct network, no action
/// - if disconnected, retry connection by passing a next_network to the
/// DISCONNECTING state
/// During this time, incoming ManualRequests are also monitored for:
/// - duplicate connect requests are deduped
/// - different connect requests are serviced by passing a next_network to the DISCONNECTING state
/// - disconnect requests cause a transition to DISCONNECTING state
async fn connected_state(
mut common_options: CommonStateOptions,
mut options: ConnectedOptions,
) -> Result<State, ExitReason> {
debug!("Entering connected state");
let mut connect_start_time = fasync::Time::now();
// Initialize connection data
let past_connections = common_options
.saved_networks_manager
.get_past_connections(
&options.currently_fulfilled_request.target.network,
&options.currently_fulfilled_request.target.credential,
&options.latest_ap_state.bssid,
)
.await;
let mut bss_quality_data = bss_selection::BssQualityData::new(
bss_selection::SignalData::new(
options.latest_ap_state.rssi_dbm,
options.latest_ap_state.snr_db,
bss_selection::EWMA_SMOOTHING_FACTOR,
),
options.latest_ap_state.channel,
past_connections,
);
// Keep track of the connection's average signal strength for future scoring.
let mut avg_rssi = SignalStrengthAverage::new();
loop {
select! {
event = options.connect_txn_stream.next() => match event {
Some(Ok(event)) => {
let is_sme_idle = match event {
fidl_sme::ConnectTransactionEvent::OnDisconnect { info: fidl_info } => {
// Log a disconnect in Cobalt
common_options.cobalt_api.log_event(
DISCONNECTION_METRIC_ID,
types::DisconnectReason::DisconnectDetectedFromSme
);
let now = fasync::Time::now();
let info = DisconnectInfo {
connected_duration: now - connect_start_time,
is_sme_reconnecting: fidl_info.is_sme_reconnecting,
disconnect_source: fidl_info.disconnect_source,
latest_ap_state: (*options.latest_ap_state).clone(),
};
common_options.telemetry_sender.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info });
// Record data about the connection and disconnect for future network
// selection.
record_disconnect(
&common_options,
&options,
connect_start_time,
types::DisconnectReason::DisconnectDetectedFromSme,
bss_quality_data.signal_data.clone(),
).await;
!fidl_info.is_sme_reconnecting
}
fidl_sme::ConnectTransactionEvent::OnConnectResult { result } => {
let connected = result.code == fidl_ieee80211::StatusCode::Success;
if connected {
// This OnConnectResult should be for reconnecting to the same AP,
// so keep the same SignalData but reset the connect start time
// to track as a new connection.
connect_start_time = fasync::Time::now();
}
common_options.telemetry_sender.send(TelemetryEvent::ConnectResult {
iface_id: common_options.iface_id,
result,
// It's not necessarily true that there are still multiple BSS
// candidates in the network at this point in time, but we use the
// heuristic that if previously there were multiple BSS's, then
// it likely remains the same.
multiple_bss_candidates: options.multiple_bss_candidates,
latest_ap_state: (*options.latest_ap_state).clone(),
});
!connected
}
fidl_sme::ConnectTransactionEvent::OnSignalReport { ind } => {
// Update connection data
options.latest_ap_state.rssi_dbm = ind.rssi_dbm;
options.latest_ap_state.snr_db = ind.snr_db;
bss_quality_data.signal_data.update_with_new_measurement(ind.rssi_dbm, ind.snr_db);
avg_rssi.add(DecibelMilliWatt(ind.rssi_dbm));
let current_connection = &options.currently_fulfilled_request.target;
handle_connection_stats(
&mut common_options.telemetry_sender,
&mut common_options.stats_sender,
common_options.iface_id,
current_connection.network.clone(),
ind,
bss_quality_data.clone()
).await;
// Evaluate current BSS, and determine if roaming future should be
// triggered.
let (_bss_score, roam_reasons) = bss_selection::evaluate_current_bss(bss_quality_data.clone());
if !roam_reasons.is_empty() {
common_options.telemetry_sender.send(TelemetryEvent::RoamingScan);
// TODO(haydennix): Trigger roaming future, which must be idempotent
// since repeated calls are likely.
}
false
}
fidl_sme::ConnectTransactionEvent::OnChannelSwitched { info } => {
options.latest_ap_state.channel.primary = info.new_channel;
common_options.telemetry_sender.send(TelemetryEvent::OnChannelSwitched { info });
false
}
};
if is_sme_idle {
let next_connecting_options = ConnectingOptions {
connect_responder: None,
connect_request: types::ConnectRequest {
reason: types::ConnectReason::RetryAfterDisconnectDetected,
target: types::ConnectionCandidate {
// strip out the bss info to force a new scan
scanned: None,
..options.currently_fulfilled_request.target.clone()
}
},
attempt_counter: 0,
};
let options = DisconnectingOptions {
disconnect_responder: None,
previous_network: Some((options.currently_fulfilled_request.target.network.clone(), types::DisconnectStatus::ConnectionFailed)),
next_network: Some(next_connecting_options),
reason: types::DisconnectReason::DisconnectDetectedFromSme,
};
common_options.telemetry_sender.send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
info!("Detected disconnection from network, will attempt reconnection");
return Ok(disconnecting_state(common_options, options).into_state());
}
}
_ => {
info!("SME dropped ConnectTransaction channel. Exiting state machine");
return Err(ExitReason(Err(format_err!("Failed to receive ConnectTransactionEvent for SME status"))));
}
},
req = common_options.req_stream.next() => {
let now = fasync::Time::now();
match req {
Some(ManualRequest::Disconnect((reason, responder))) => {
debug!("Disconnect requested");
record_disconnect(
&common_options,
&options,
connect_start_time,
reason,
bss_quality_data.signal_data
).await;
let latest_ap_state = options.latest_ap_state;
let options = DisconnectingOptions {
disconnect_responder: Some(responder),
previous_network: Some((options.currently_fulfilled_request.target.network, types::DisconnectStatus::ConnectionStopped)),
next_network: None,
reason,
};
common_options.cobalt_api.log_event(DISCONNECTION_METRIC_ID, options.reason);
let info = DisconnectInfo {
connected_duration: now - connect_start_time,
is_sme_reconnecting: false,
disconnect_source: fidl_sme::DisconnectSource::User(types::convert_to_sme_disconnect_reason(options.reason)),
latest_ap_state: *latest_ap_state,
};
common_options.telemetry_sender.send(TelemetryEvent::Disconnected { track_subsequent_downtime: false, info });
return Ok(disconnecting_state(common_options, options).into_state());
}
Some(ManualRequest::Connect((new_connect_request, new_responder))) => {
// Check if it's the same network as we're currently connected to. If yes, reply immediately
if new_connect_request.target.network == options.currently_fulfilled_request.target.network {
info!("Received connection request for current network, deduping");
new_responder.send(()).unwrap_or_else(|_| ());
} else {
let disconnect_reason = convert_manual_connect_to_disconnect_reason(&new_connect_request.reason).unwrap_or_else(|()| {
error!("Unexpected connection reason: {:?}", new_connect_request.reason);
types::DisconnectReason::Unknown
});
record_disconnect(
&common_options,
&options,
connect_start_time,
disconnect_reason,
bss_quality_data.signal_data
).await;
let next_connecting_options = ConnectingOptions {
connect_responder: Some(new_responder),
connect_request: new_connect_request.clone(),
attempt_counter: 0,
};
let latest_ap_state = options.latest_ap_state;
let options = DisconnectingOptions {
disconnect_responder: None,
previous_network: Some((options.currently_fulfilled_request.target.network, types::DisconnectStatus::ConnectionStopped)),
next_network: Some(next_connecting_options),
reason: disconnect_reason,
};
info!("Connection to new network requested, disconnecting from current network");
common_options.cobalt_api.log_event(DISCONNECTION_METRIC_ID, options.reason);
let info = DisconnectInfo {
connected_duration: now - connect_start_time,
is_sme_reconnecting: false,
disconnect_source: fidl_sme::DisconnectSource::User(types::convert_to_sme_disconnect_reason(options.reason)),
latest_ap_state: *latest_ap_state,
};
common_options.telemetry_sender.send(TelemetryEvent::Disconnected { track_subsequent_downtime: false, info });
return Ok(disconnecting_state(common_options, options).into_state())
}
}
None => return handle_none_request(),
};
}
}
}
}
/// Update IfaceManager with the updated connection quality data.
async fn handle_connection_stats(
telemetry_sender: &mut TelemetrySender,
stats_sender: &mut ConnectionStatsSender,
iface_id: u16,
id: types::NetworkIdentifier,
ind: fidl_internal::SignalReportIndication,
bss_quality_data: bss_selection::BssQualityData,
) {
let connection_stats =
PeriodicConnectionStats { id, iface_id, quality_data: bss_quality_data.clone() };
stats_sender.unbounded_send(connection_stats).unwrap_or_else(|e| {
error!("Failed to send periodic connection stats from the connected state: {}", e);
});
// Send RSSI and RSSI velocity metrics
telemetry_sender.send(TelemetryEvent::OnSignalReport {
ind,
rssi_velocity: bss_quality_data.signal_data.rssi_velocity,
});
}
async fn record_disconnect(
common_options: &CommonStateOptions,
options: &ConnectedOptions,
connect_start_time: fasync::Time,
reason: types::DisconnectReason,
signal_data: bss_selection::SignalData,
) {
let curr_time = fasync::Time::now();
let uptime = curr_time - connect_start_time;
let data = PastConnectionData::new(
options.latest_ap_state.bssid,
options.connection_attempt_time,
options.time_to_connect,
curr_time,
uptime,
reason,
signal_data,
// TODO: record average phy rate over connection once available
0,
);
common_options
.saved_networks_manager
.record_disconnect(
&options.currently_fulfilled_request.target.network.clone().into(),
&options.currently_fulfilled_request.target.credential,
data,
)
.await;
}
/// Get the disconnect reason corresponding to the connect reason. Return an error if the connect
/// reason does not correspond to a manual connect.
pub fn convert_manual_connect_to_disconnect_reason(
reason: &types::ConnectReason,
) -> Result<types::DisconnectReason, ()> {
match reason {
types::ConnectReason::FidlConnectRequest => Ok(types::DisconnectReason::FidlConnectRequest),
types::ConnectReason::ProactiveNetworkSwitch => {
Ok(types::DisconnectReason::ProactiveNetworkSwitch)
}
types::ConnectReason::RetryAfterDisconnectDetected
| types::ConnectReason::RetryAfterFailedConnectAttempt
| types::ConnectReason::RegulatoryChangeReconnect
| types::ConnectReason::IdleInterfaceAutoconnect
| types::ConnectReason::NewSavedNetworkAutoconnect => Err(()),
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::{
config_management::{
network_config::{
self, AddAndGetRecent, Credential, FailureReason, WPA_PSK_BYTE_LEN,
},
PastConnectionList, SavedNetworksManager,
},
telemetry::{TelemetryEvent, TelemetrySender},
util::{
listener,
testing::{
create_inspect_persistence_channel, create_mock_cobalt_sender,
create_mock_cobalt_sender_and_receiver, create_wlan_hasher,
generate_disconnect_info, poll_sme_req, random_connection_data,
validate_sme_scan_request_and_send_results, ConnectResultRecord,
ConnectionRecord, FakeSavedNetworksManager,
},
},
validate_cobalt_events, validate_no_cobalt_events,
},
cobalt_client::traits::AsEventCode,
fidl::endpoints::create_proxy_and_stream,
fidl::prelude::*,
fidl_fuchsia_cobalt::CobaltEvent,
fidl_fuchsia_stash as fidl_stash, fidl_fuchsia_wlan_policy as fidl_policy,
fuchsia_cobalt::CobaltEventExt,
fuchsia_inspect::{self as inspect},
fuchsia_zircon::prelude::*,
futures::{task::Poll, Future},
pin_utils::pin_mut,
std::convert::TryFrom,
test_case::test_case,
test_util::{assert_gt, assert_lt},
wlan_common::{
assert_variant, bss::Protection, random_bss_description, random_fidl_bss_description,
},
wlan_metrics_registry::PolicyDisconnectionMetricDimensionReason,
};
struct TestValues {
common_options: CommonStateOptions,
sme_req_stream: fidl_sme::ClientSmeRequestStream,
saved_networks_manager: Arc<FakeSavedNetworksManager>,
client_req_sender: mpsc::Sender<ManualRequest>,
update_receiver: mpsc::UnboundedReceiver<listener::ClientListenerMessage>,
cobalt_events: mpsc::Receiver<CobaltEvent>,
telemetry_receiver: mpsc::Receiver<TelemetryEvent>,
stats_receiver: mpsc::UnboundedReceiver<PeriodicConnectionStats>,
}
fn test_setup() -> TestValues {
let (client_req_sender, client_req_stream) = mpsc::channel(1);
let (update_sender, update_receiver) = mpsc::unbounded();
let (sme_proxy, sme_server) =
create_proxy::<fidl_sme::ClientSmeMarker>().expect("failed to create an sme channel");
let sme_req_stream = sme_server.into_stream().expect("could not create SME request stream");
let saved_networks = FakeSavedNetworksManager::new();
let saved_networks_manager = Arc::new(saved_networks);
let (cobalt_api, cobalt_events) = create_mock_cobalt_sender_and_receiver();
let (persistence_req_sender, _persistence_stream) = create_inspect_persistence_channel();
let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
let telemetry_sender = TelemetrySender::new(telemetry_sender);
let network_selector = Arc::new(network_selection::NetworkSelector::new(
saved_networks_manager.clone(),
create_mock_cobalt_sender(),
create_wlan_hasher(),
inspect::Inspector::new().root().create_child("network_selector"),
persistence_req_sender,
telemetry_sender.clone(),
));
let (stats_sender, stats_receiver) = mpsc::unbounded();
TestValues {
common_options: CommonStateOptions {
proxy: sme_proxy,
req_stream: client_req_stream.fuse(),
update_sender: update_sender,
saved_networks_manager: saved_networks_manager.clone(),
network_selector,
cobalt_api,
telemetry_sender,
iface_id: 1,
has_wpa3_support: false,
stats_sender,
},
sme_req_stream,
saved_networks_manager,
client_req_sender,
update_receiver,
cobalt_events,
telemetry_receiver,
stats_receiver,
}
}
async fn run_state_machine(
fut: impl Future<Output = Result<State, ExitReason>> + Send + 'static,
) {
let state_machine = fut.into_state_machine();
select! {
_state_machine = state_machine.fuse() => return,
}
}
fn wep_key() -> Credential {
Credential::Password("abcdef0000".as_bytes().to_vec())
}
fn wpa_password() -> Credential {
Credential::Password("password".as_bytes().to_vec())
}
fn wpa_psk() -> Credential {
Credential::Psk(vec![0u8; WPA_PSK_BYTE_LEN])
}
/// Move stash requests forward so that a save request can progress.
fn process_stash_write(
exec: &mut fasync::TestExecutor,
stash_server: &mut fidl_stash::StoreAccessorRequestStream,
) {
assert_variant!(
exec.run_until_stalled(&mut stash_server.try_next()),
Poll::Ready(Ok(Some(fidl_stash::StoreAccessorRequest::SetValue { .. })))
);
assert_variant!(
exec.run_until_stalled(&mut stash_server.try_next()),
Poll::Ready(Ok(Some(fidl_stash::StoreAccessorRequest::Flush{responder}))) => {
responder.send(&mut Ok(())).expect("failed to send stash response");
}
);
}
#[fuchsia::test]
fn connecting_state_successfully_connects() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
let mut test_values = test_setup();
// Do SavedNetworksManager set up manually to get functionality and stash server
let (saved_networks, mut stash_server) =
exec.run_singlethreaded(SavedNetworksManager::new_and_stash_server());
let saved_networks_manager = Arc::new(saved_networks);
test_values.common_options.saved_networks_manager = saved_networks_manager.clone();
let next_network_ssid = types::Ssid::try_from("bar").unwrap();
let bss_description = random_fidl_bss_description!(Wpa2, ssid: next_network_ssid.clone());
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: types::SecurityType::Wep,
},
credential: Credential::Password("five0".as_bytes().to_vec()),
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Wep,
}),
},
reason: types::ConnectReason::FidlConnectRequest,
};
// Store the network in the saved_networks_manager, so we can record connection success
let save_fut = saved_networks_manager.store(
connect_request.target.network.clone().into(),
connect_request.target.credential.clone(),
);
pin_mut!(save_fut);
assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Pending);
process_stash_write(&mut exec, &mut stash_server);
assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Ready(Ok(None)));
// Check that the saved networks manager has the expected initial data
let saved_networks = exec.run_singlethreaded(
saved_networks_manager.lookup(&connect_request.target.network.clone().into()),
);
assert_eq!(false, saved_networks[0].has_ever_connected);
assert!(saved_networks[0].hidden_probability > 0.0);
let (connect_sender, mut connect_receiver) = oneshot::channel();
let connecting_options = ConnectingOptions {
connect_responder: Some(connect_sender),
connect_request: connect_request.clone(),
attempt_counter: 0,
};
let initial_state = connecting_state(test_values.common_options, connecting_options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
let sme_fut = test_values.sme_req_stream.into_future();
pin_mut!(sme_fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure a connect request is sent to the SME
let connect_txn_handle = assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
assert_eq!(req.ssid, next_network_ssid.to_vec());
assert_eq!(connect_request.target.credential, req.authentication.credentials.into());
assert_eq!(req.bss_description, bss_description);
assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
assert_eq!(req.multiple_bss_candidates, true);
// Send connection response.
let (_stream, ctrl) = txn.expect("connect txn unused")
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl
}
);
connect_txn_handle
.send_on_connect_result(&mut fake_successful_connect_result())
.expect("failed to send connection completion");
// Check for a connecting update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: types::SecurityType::Wep,
},
state: fidl_policy::ConnectionState::Connecting,
status: None,
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
process_stash_write(&mut exec, &mut stash_server);
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check the responder was acknowledged
assert_variant!(exec.run_until_stalled(&mut connect_receiver), Poll::Ready(Ok(())));
// Check for a connect update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: types::SecurityType::Wep,
},
state: fidl_policy::ConnectionState::Connected,
status: None,
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Check that the saved networks manager has the connection recorded
let saved_networks = exec.run_singlethreaded(
saved_networks_manager.lookup(&connect_request.target.network.clone().into()),
);
assert_eq!(true, saved_networks[0].has_ever_connected);
assert_eq!(
network_config::PROB_HIDDEN_IF_CONNECT_PASSIVE,
saved_networks[0].hidden_probability
);
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure no further updates were sent to listeners
assert_variant!(
exec.run_until_stalled(&mut test_values.update_receiver.into_future()),
Poll::Pending
);
// Cobalt metrics logged
validate_cobalt_events!(
test_values.cobalt_events,
CONNECTION_ATTEMPT_METRIC_ID,
types::ConnectReason::FidlConnectRequest
);
}
#[fuchsia::test]
fn connecting_state_successfully_scans_and_connects() {
let mut exec =
fasync::TestExecutor::new_with_fake_time().expect("failed to create an executor");
exec.set_fake_time(fasync::Time::from_nanos(123));
let mut test_values = test_setup();
let next_network_ssid = types::Ssid::try_from("bar").unwrap();
let id = types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: types::SecurityType::Wep,
};
let credential = Credential::Password("12345".as_bytes().to_vec());
let connection_attempt_time = fasync::Time::now();
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: id.clone(),
credential: credential.clone(),
scanned: None,
},
reason: types::ConnectReason::FidlConnectRequest,
};
// Set how the SavedNetworksManager should respond to lookup_compatible for the scan.
let expected_config =
network_config::NetworkConfig::new(id.clone().into(), credential.clone(), false)
.expect("failed to create network config");
test_values.saved_networks_manager.set_lookup_compatible_response(vec![expected_config]);
let (connect_sender, mut connect_receiver) = oneshot::channel();
let connecting_options = ConnectingOptions {
connect_responder: Some(connect_sender),
connect_request: connect_request.clone(),
attempt_counter: 0,
};
let initial_state = connecting_state(test_values.common_options, connecting_options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure a scan request is sent to the SME and send back a result
let expected_scan_request = fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
ssids: vec![next_network_ssid.to_vec()],
channels: vec![],
});
let bss_description = random_fidl_bss_description!(Wep, ssid: next_network_ssid.clone());
let scan_results = vec![fidl_sme::ScanResult {
compatible: true,
timestamp_nanos: zx::Time::get_monotonic().into_nanos(),
bss_description: bss_description.clone(),
}];
validate_sme_scan_request_and_send_results(
&mut exec,
&mut test_values.sme_req_stream,
&expected_scan_request,
scan_results,
);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure a connect request is sent to the SME
let sme_fut = test_values.sme_req_stream.into_future();
pin_mut!(sme_fut);
let time_to_connect = 30.seconds();
let connect_txn_handle = assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
assert_eq!(req.ssid, next_network_ssid.to_vec());
assert_eq!(connect_request.target.credential, req.authentication.credentials.into());
assert_eq!(req.bss_description, bss_description.clone().into());
assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
assert_eq!(req.multiple_bss_candidates, false);
// Send connection response.
exec.set_fake_time(fasync::Time::after(time_to_connect));
let (_stream, ctrl) = txn.expect("connect txn unused")
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl
}
);
connect_txn_handle
.send_on_connect_result(&mut fake_successful_connect_result())
.expect("failed to send connection completion");
// Check for a connecting update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: types::SecurityType::Wep,
},
state: fidl_policy::ConnectionState::Connecting,
status: None,
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Check that NetworkSelectionDecision telemetry event is sent
assert_variant!(test_values.telemetry_receiver.try_next(), Ok(Some(event)) => {
assert_variant!(event, TelemetryEvent::NetworkSelectionDecision { .. });
});
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check the responder was acknowledged
assert_variant!(exec.run_until_stalled(&mut connect_receiver), Poll::Ready(Ok(())));
// Check for a connect update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: types::SecurityType::Wep,
},
state: fidl_policy::ConnectionState::Connected,
status: None,
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Check that the saved networks manager has the connection result recorded
assert_variant!(test_values.saved_networks_manager.get_recorded_connect_reslts().as_slice(), [data] => {
let expected_connect_result = ConnectResultRecord {
id: id.clone(),
credential: credential.clone(),
bssid: types::Bssid(bss_description.bssid),
connect_result: fake_successful_connect_result(),
discovered_in_scan: None,
};
assert_eq!(data, &expected_connect_result);
});
// Check that connected telemetry event is sent
assert_variant!(
test_values.telemetry_receiver.try_next(),
Ok(Some(TelemetryEvent::ConnectResult { iface_id: 1, result, multiple_bss_candidates, latest_ap_state })) => {
assert_eq!(bss_description, latest_ap_state.into());
assert!(!multiple_bss_candidates);
assert_eq!(result, fake_successful_connect_result());
}
);
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure no further updates were sent to listeners
assert_variant!(
exec.run_until_stalled(&mut test_values.update_receiver.into_future()),
Poll::Pending
);
// Cobalt metrics logged
validate_cobalt_events!(
test_values.cobalt_events,
CONNECTION_ATTEMPT_METRIC_ID,
types::ConnectReason::FidlConnectRequest
);
// Send a disconnect and check that the connection data is correctly recorded
let is_sme_reconnecting = false;
let mut fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting);
connect_txn_handle
.send_on_disconnect(&mut fidl_disconnect_info)
.expect("failed to send disconnection event");
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
let expected_recorded_connection = ConnectionRecord {
id: id.clone(),
credential: credential.clone(),
data: PastConnectionData {
bssid: types::Bssid(bss_description.bssid),
connection_attempt_time,
time_to_connect,
disconnect_time: fasync::Time::now(),
connection_uptime: zx::Duration::from_minutes(0),
disconnect_reason: types::DisconnectReason::DisconnectDetectedFromSme,
signal_data_at_disconnect: bss_selection::SignalData::new(
bss_description.rssi_dbm,
bss_description.snr_db,
bss_selection::EWMA_SMOOTHING_FACTOR,
),
// TODO: record average phy rate over connection once available
average_tx_rate: 0,
},
};
assert_variant!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [data] => {
assert_eq!(data, &expected_recorded_connection);
});
}
#[test_case(types::SecurityType::Wpa3)]
#[test_case(types::SecurityType::Wpa2)]
#[fuchsia::test(add_test_attr = false)]
fn connecting_state_successfully_connects_wpa2wpa3(type_: types::SecurityType) {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
// Do test set up manually to get stash server
let (_client_req_sender, client_req_stream) = mpsc::channel(1);
let (update_sender, _update_receiver) = mpsc::unbounded();
let (sme_proxy, sme_server) =
create_proxy::<fidl_sme::ClientSmeMarker>().expect("failed to create an sme channel");
let mut sme_req_stream =
sme_server.into_stream().expect("could not create SME request stream");
let (saved_networks, mut stash_server) =
exec.run_singlethreaded(SavedNetworksManager::new_and_stash_server());
let saved_networks_manager = Arc::new(saved_networks);
let (cobalt_api, _cobalt_events) = create_mock_cobalt_sender_and_receiver();
let (persistence_req_sender, _persistence_stream) = create_inspect_persistence_channel();
let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
let telemetry_sender = TelemetrySender::new(telemetry_sender);
let (stats_sender, _stats_receiver) = mpsc::unbounded();
let network_selector = Arc::new(network_selection::NetworkSelector::new(
saved_networks_manager.clone(),
create_mock_cobalt_sender(),
create_wlan_hasher(),
inspect::Inspector::new().root().create_child("network_selector"),
persistence_req_sender,
telemetry_sender.clone(),
));
let next_network_ssid = types::Ssid::try_from("bar").unwrap();
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: type_.into(),
},
credential: Credential::Password("Anything".as_bytes().to_vec()),
scanned: None,
},
reason: types::ConnectReason::FidlConnectRequest,
};
// Store the network in the saved_networks_manager, so we can record connection success
let save_fut = saved_networks_manager.store(
connect_request.target.network.clone().into(),
connect_request.target.credential.clone(),
);
pin_mut!(save_fut);
assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Pending);
process_stash_write(&mut exec, &mut stash_server);
assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Ready(Ok(None)));
// Check that the saved networks manager has the expected initial data
let saved_networks = exec.run_singlethreaded(
saved_networks_manager.lookup(&connect_request.target.network.clone().into()),
);
assert_eq!(false, saved_networks[0].has_ever_connected);
assert!(saved_networks[0].hidden_probability > 0.0);
let (connect_sender, _connect_receiver) = oneshot::channel();
let connecting_options = ConnectingOptions {
connect_responder: Some(connect_sender),
connect_request: connect_request.clone(),
attempt_counter: 0,
};
let common_options = CommonStateOptions {
proxy: sme_proxy,
req_stream: client_req_stream.fuse(),
update_sender: update_sender,
saved_networks_manager: saved_networks_manager.clone(),
network_selector,
cobalt_api: cobalt_api,
telemetry_sender,
iface_id: 1,
has_wpa3_support: false,
stats_sender,
};
let initial_state = connecting_state(common_options, connecting_options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure a scan request is sent to the SME and send back a result
let expected_scan_request = fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
ssids: vec![next_network_ssid.to_vec()],
channels: vec![],
});
let bss_description =
random_fidl_bss_description!(Wpa2Wpa3, ssid: next_network_ssid.clone());
let scan_results = vec![fidl_sme::ScanResult {
compatible: true,
timestamp_nanos: zx::Time::get_monotonic().into_nanos(),
bss_description: bss_description.clone(),
}];
validate_sme_scan_request_and_send_results(
&mut exec,
&mut sme_req_stream,
&expected_scan_request,
scan_results,
);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure the WPA2/WPA3 network was selected for connection and a connect request is sent to
// the SME.
let sme_fut = sme_req_stream.into_future();
pin_mut!(sme_fut);
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, .. }) => {
assert_eq!(req.ssid, next_network_ssid.to_vec());
assert_eq!(connect_request.target.credential, req.authentication.credentials.into());
assert_eq!(req.bss_description, bss_description);
assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
}
);
}
/// Test parameters for authentication test cases.
///
/// See the `connecting_state_select_authentication` test function.
#[derive(Clone, Debug)]
struct AuthenticationTestCase {
credential: Credential,
requested: types::SecurityType,
scanned: types::SecurityTypeDetailed,
has_wpa3_support: bool,
}
impl AuthenticationTestCase {
fn open() -> Self {
AuthenticationTestCase {
credential: Credential::None,
requested: types::SecurityType::None,
scanned: types::SecurityTypeDetailed::Open,
has_wpa3_support: false,
}
}
fn wep_requested_wpa1_scanned() -> Self {
AuthenticationTestCase {
credential: wep_key(),
requested: types::SecurityType::Wep,
scanned: types::SecurityTypeDetailed::Wpa1,
has_wpa3_support: false,
}
}
fn wpa1_requested_wpa2_scanned() -> Self {
AuthenticationTestCase {
credential: wpa_psk(),
requested: types::SecurityType::Wpa,
scanned: types::SecurityTypeDetailed::Wpa2Personal,
has_wpa3_support: false,
}
}
fn wpa2_requested_open_scanned() -> Self {
AuthenticationTestCase {
credential: wpa_password(),
requested: types::SecurityType::Wpa2,
scanned: types::SecurityTypeDetailed::Open,
has_wpa3_support: false,
}
}
fn wpa2_requested_wpa3_scanned(
has_wpa3_credential_support: bool,
has_wpa3_hardware_support: bool,
) -> Self {
AuthenticationTestCase {
credential: if has_wpa3_credential_support { wpa_password() } else { wpa_psk() },
requested: types::SecurityType::Wpa2,
scanned: types::SecurityTypeDetailed::Wpa3Personal,
has_wpa3_support: has_wpa3_hardware_support,
}
}
fn wpa3_requested_wpa2_wpa3_scanned(has_wpa3_hardware_support: bool) -> Self {
AuthenticationTestCase {
credential: wpa_password(),
requested: types::SecurityType::Wpa3,
scanned: types::SecurityTypeDetailed::Wpa2Wpa3Personal,
has_wpa3_support: has_wpa3_hardware_support,
}
}
fn wpa2_wpa3_scanned(
requested: types::SecurityType,
has_wpa3_credential_support: bool,
has_wpa3_hardware_support: bool,
) -> Self {
AuthenticationTestCase {
credential: if has_wpa3_credential_support { wpa_password() } else { wpa_psk() },
requested,
scanned: types::SecurityTypeDetailed::Wpa2Wpa3Personal,
has_wpa3_support: has_wpa3_hardware_support,
}
}
}
// TODO(fxbug.dev/102196): Refactor this test into an a more end-to-end test against the higher
// level client API (rather than testing directly against the state
// machine).
/// Tests for success and failure based on authentication parameters.
///
/// This test exercises authentication (security protocol) selection in the state machine. The
/// parameters in `AuthenticationTestCase` determine the security protocol and/or credentials
/// of the network to which a connection is requested, the corresponding saved network, and the
/// network discovered during the scan.
// Expect successful connection for the following cases.
#[test_case(AuthenticationTestCase::open())]
#[test_case(AuthenticationTestCase::wpa1_requested_wpa2_scanned())]
#[test_case(AuthenticationTestCase::wpa2_requested_wpa3_scanned(true, true))]
#[test_case(AuthenticationTestCase::wpa3_requested_wpa2_wpa3_scanned(false))]
#[test_case(AuthenticationTestCase::wpa3_requested_wpa2_wpa3_scanned(true))]
#[test_case(AuthenticationTestCase::wpa2_wpa3_scanned(
types::SecurityType::Wpa2,
false,
false
))]
#[test_case(AuthenticationTestCase::wpa2_wpa3_scanned(types::SecurityType::Wpa2, true, false))]
#[test_case(AuthenticationTestCase::wpa2_wpa3_scanned(types::SecurityType::Wpa2, false, true))]
#[test_case(AuthenticationTestCase::wpa2_wpa3_scanned(types::SecurityType::Wpa2, true, true))]
#[test_case(AuthenticationTestCase::wpa2_wpa3_scanned(types::SecurityType::Wpa3, true, false))]
#[test_case(AuthenticationTestCase::wpa2_wpa3_scanned(types::SecurityType::Wpa3, true, true))]
// Expect unsuccessful connection (panic) for the following cases.
#[test_case(AuthenticationTestCase::wep_requested_wpa1_scanned() => panics)]
#[test_case(AuthenticationTestCase::wpa2_requested_open_scanned() => panics)]
#[test_case(AuthenticationTestCase::wpa2_requested_wpa3_scanned(false, false) => panics)]
#[test_case(AuthenticationTestCase::wpa2_requested_wpa3_scanned(true, false) => panics)]
#[test_case(AuthenticationTestCase::wpa2_requested_wpa3_scanned(false, true) => panics)]
#[fuchsia::test(add_test_attr = false)]
fn connecting_state_select_authentication(case: AuthenticationTestCase) {
let mut executor = fasync::TestExecutor::new().expect("failed to create an executor");
// Configure channels and WLAN components for the test. This test must save networks, so it
// does not use the common setup functions seen in other tests in this module.
let (_client_req_tx, client_req_rx) = mpsc::channel(1);
let (update_tx, _update_rx) = mpsc::unbounded();
let (sme_proxy, sme_server) =
create_proxy::<fidl_sme::ClientSmeMarker>().expect("failed to create an sme channel");
let mut sme_req_stream =
sme_server.into_stream().expect("could not create SME request stream");
let (saved_networks, mut stash_server) =
executor.run_singlethreaded(SavedNetworksManager::new_and_stash_server());
let saved_networks_manager = Arc::new(saved_networks);
let (cobalt_tx, _cobalt_rx) = create_mock_cobalt_sender_and_receiver();
let (persistence_tx, _persistence_rx) = create_inspect_persistence_channel();
let (telementry_tx, _telemetry_rx) = mpsc::channel::<TelemetryEvent>(100);
let telemetry_sender = TelemetrySender::new(telementry_tx);
let (stats_tx, _stats_rx) = mpsc::unbounded();
let network_selector = Arc::new(network_selection::NetworkSelector::new(
saved_networks_manager.clone(),
create_mock_cobalt_sender(),
create_wlan_hasher(),
inspect::Inspector::new().root().create_child("network_selector"),
persistence_tx,
telemetry_sender.clone(),
));
// Create an SSID and connect request for the requested network of the test case.
let ssid = types::Ssid::try_from("test").unwrap();
let id = types::NetworkIdentifier { ssid: ssid.clone(), security_type: case.requested };
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: id.clone(),
credential: case.credential.clone(),
scanned: None,
},
reason: types::ConnectReason::FidlConnectRequest,
};
// Store the requested network of the test case.
let store_future = saved_networks_manager.store(id, case.credential);
pin_mut!(store_future);
assert_variant!(executor.run_until_stalled(&mut store_future), Poll::Pending);
process_stash_write(&mut executor, &mut stash_server);
assert_variant!(executor.run_until_stalled(&mut store_future), Poll::Ready(Ok(None)));
// Create a state machine in the connecting state and begin running it.
let (connect_tx, _connect_rx) = oneshot::channel();
let connecting_options = ConnectingOptions {
connect_responder: Some(connect_tx),
connect_request: connect_request.clone(),
attempt_counter: 0,
};
let common_options = CommonStateOptions {
proxy: sme_proxy,
req_stream: client_req_rx.fuse(),
update_sender: update_tx,
saved_networks_manager: saved_networks_manager.clone(),
network_selector,
cobalt_api: cobalt_tx,
telemetry_sender,
iface_id: 1,
has_wpa3_support: case.has_wpa3_support,
stats_sender: stats_tx,
};
let initial_state = connecting_state(common_options, connecting_options);
let run_state_machine_future = run_state_machine(initial_state);
pin_mut!(run_state_machine_future);
assert_variant!(executor.run_until_stalled(&mut run_state_machine_future), Poll::Pending);
// Report a network with the test case SSID and security protocol in the scan result from
// SME.
let expected_scan_request = fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
ssids: vec![ssid.to_vec()],
channels: vec![],
});
let bss_description =
random_fidl_bss_description!(protection => case.scanned, ssid: ssid.clone());
let scan_results = vec![fidl_sme::ScanResult {
compatible: match case.scanned {
types::SecurityTypeDetailed::Wpa3Personal
| types::SecurityTypeDetailed::Wpa3Enterprise => case.has_wpa3_support,
_ => true,
},
timestamp_nanos: zx::Time::get_monotonic().into_nanos(),
bss_description: bss_description.clone(),
}];
validate_sme_scan_request_and_send_results(
&mut executor,
&mut sme_req_stream,
&expected_scan_request,
scan_results,
);
assert_variant!(executor.run_until_stalled(&mut run_state_machine_future), Poll::Pending);
// Assert that SME is sent a connect request.
let sme_req_future = sme_req_stream.into_future();
pin_mut!(sme_req_future);
assert_variant!(
poll_sme_req(&mut executor, &mut sme_req_future),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, .. }) => {
assert_eq!(req.ssid, ssid.to_vec());
assert_eq!(connect_request.target.credential, req.authentication.credentials.into());
assert_eq!(req.bss_description, bss_description);
assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
}
);
}
// TODO(fxbug.dev/92693): Test with WPA Enterprise security protocols.
/// Tests for success when forwarding an authentication method in a scanned candidate.
#[test_case(Credential::None, types::SecurityTypeDetailed::Open, false)]
#[test_case(wep_key(), types::SecurityTypeDetailed::Wep, false)]
#[test_case(wpa_psk(), types::SecurityTypeDetailed::Wpa1, false)]
#[test_case(wpa_psk(), types::SecurityTypeDetailed::Wpa1Wpa2PersonalTkipOnly, false)]
#[test_case(wpa_psk(), types::SecurityTypeDetailed::Wpa1Wpa2Personal, false)]
#[test_case(wpa_psk(), types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, false)]
#[test_case(wpa_psk(), types::SecurityTypeDetailed::Wpa2Personal, false)]
#[test_case(wpa_password(), types::SecurityTypeDetailed::Wpa2Wpa3Personal, false)]
#[test_case(wpa_psk(), types::SecurityTypeDetailed::Wpa2Wpa3Personal, false)]
#[test_case(wpa_password(), types::SecurityTypeDetailed::Wpa2Wpa3Personal, true)]
#[test_case(wpa_psk(), types::SecurityTypeDetailed::Wpa2Wpa3Personal, true)]
#[test_case(wpa_password(), types::SecurityTypeDetailed::Wpa3Personal, true)]
#[fuchsia::test(add_test_attr = false)]
fn connecting_state_forward_authentication(
credential: Credential,
scanned: types::SecurityTypeDetailed,
has_wpa3_support: bool,
) {
let mut executor = fasync::TestExecutor::new().expect("failed to create an executor");
// Configure channels and WLAN components for the test. This test must save networks, so it
// does not use the common setup functions seen in other tests in this module.
let (_client_req_tx, client_req_rx) = mpsc::channel(1);
let (update_tx, _update_rx) = mpsc::unbounded();
let (sme_proxy, sme_server) =
create_proxy::<fidl_sme::ClientSmeMarker>().expect("failed to create an sme channel");
let sme_req_stream = sme_server.into_stream().expect("could not create SME request stream");
let (saved_networks, mut stash_server) =
executor.run_singlethreaded(SavedNetworksManager::new_and_stash_server());
let saved_networks_manager = Arc::new(saved_networks);
let (cobalt_tx, _cobalt_rx) = create_mock_cobalt_sender_and_receiver();
let (persistence_tx, _persistence_rx) = create_inspect_persistence_channel();
let (telementry_tx, _telemetry_rx) = mpsc::channel::<TelemetryEvent>(100);
let telemetry_sender = TelemetrySender::new(telementry_tx);
let (stats_tx, _stats_rx) = mpsc::unbounded();
let network_selector = Arc::new(network_selection::NetworkSelector::new(
saved_networks_manager.clone(),
create_mock_cobalt_sender(),
create_wlan_hasher(),
inspect::Inspector::new().root().create_child("network_selector"),
persistence_tx,
telemetry_sender.clone(),
));
// Create an SSID and connect request for the requested network of the test case. Note that
// the security types of the network identifier and BSS description need not corroborate
// the scanned candidate; these security protocol specifications should not interact in the
// state machine when a scanned candidate is provided.
let ssid = types::Ssid::try_from("test").unwrap();
let id = types::NetworkIdentifier {
ssid: ssid.clone(),
security_type: match credential {
Credential::None => types::SecurityType::None,
Credential::Password(_) | Credential::Psk(_) => types::SecurityType::Wpa,
},
};
let bss_description = random_fidl_bss_description!(Open, ssid: ssid.clone());
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: id.clone(),
credential: credential.clone(),
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone(),
observation: types::ScanObservation::Active,
has_multiple_bss_candidates: false,
security_type_detailed: scanned,
}),
},
reason: types::ConnectReason::FidlConnectRequest,
};
// Store the requested network of the test case.
let store_future = saved_networks_manager.store(id, credential);
pin_mut!(store_future);
assert_variant!(executor.run_until_stalled(&mut store_future), Poll::Pending);
process_stash_write(&mut executor, &mut stash_server);
assert_variant!(executor.run_until_stalled(&mut store_future), Poll::Ready(Ok(None)));
// Create a state machine in the connecting state and begin running it.
let (connect_tx, _connect_rx) = oneshot::channel();
let connecting_options = ConnectingOptions {
connect_responder: Some(connect_tx),
connect_request: connect_request.clone(),
attempt_counter: 0,
};
let common_options = CommonStateOptions {
proxy: sme_proxy,
req_stream: client_req_rx.fuse(),
update_sender: update_tx,
saved_networks_manager: saved_networks_manager.clone(),
network_selector,
cobalt_api: cobalt_tx,
telemetry_sender,
iface_id: 1,
has_wpa3_support,
stats_sender: stats_tx,
};
let initial_state = connecting_state(common_options, connecting_options);
let run_state_machine_future = run_state_machine(initial_state);
pin_mut!(run_state_machine_future);
assert_variant!(executor.run_until_stalled(&mut run_state_machine_future), Poll::Pending);
// Assert that SME is sent a connect request.
let sme_req_future = sme_req_stream.into_future();
pin_mut!(sme_req_future);
assert_variant!(
poll_sme_req(&mut executor, &mut sme_req_future),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, .. }) => {
assert_eq!(req.ssid, ssid.to_vec());
assert_eq!(connect_request.target.credential, req.authentication.credentials.into());
assert_eq!(req.bss_description, bss_description);
assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
}
);
}
#[fuchsia::test]
fn connecting_state_fails_to_connect_and_retries() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
let mut test_values = test_setup();
let next_network_ssid = types::Ssid::try_from("bar").unwrap();
let bss_description = random_bss_description!(Wpa2, ssid: next_network_ssid.clone());
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
credential: Credential::Password(b"password".to_vec()),
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone().into(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal,
}),
},
reason: types::ConnectReason::FidlConnectRequest,
};
let (connect_sender, mut connect_receiver) = oneshot::channel();
let connecting_options = ConnectingOptions {
connect_responder: Some(connect_sender),
connect_request: connect_request.clone(),
attempt_counter: 0,
};
let initial_state = connecting_state(test_values.common_options, connecting_options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
let sme_fut = test_values.sme_req_stream.into_future();
pin_mut!(sme_fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check the responder was acknowledged
assert_variant!(exec.run_until_stalled(&mut connect_receiver), Poll::Ready(Ok(())));
// Ensure a connect request is sent to the SME
let mut connect_txn_handle = assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
assert_eq!(req.ssid, next_network_ssid.to_vec());
// Send connection response.
let (_stream, ctrl) = txn.expect("connect txn unused")
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl
}
);
let mut connect_result = fidl_sme::ConnectResult {
code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
..fake_successful_connect_result()
};
connect_txn_handle
.send_on_connect_result(&mut connect_result)
.expect("failed to send connection completion");
// Check for a connecting update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Connecting,
status: None,
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
assert!(exec.wake_next_timer().is_some());
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check that connect result telemetry event is sent
assert_variant!(
test_values.telemetry_receiver.try_next(),
Ok(Some(TelemetryEvent::ConnectResult { iface_id: 1, result, multiple_bss_candidates, latest_ap_state })) => {
assert_eq!(bss_description, latest_ap_state);
assert!(multiple_bss_candidates);
assert_eq!(result, connect_result);
}
);
// Ensure a disconnect request is sent to the SME
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::FailedToConnect }) => {
responder.send().expect("could not send sme response");
}
);
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure a connect request is sent to the SME
connect_txn_handle = assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
assert_eq!(req.ssid, next_network_ssid.to_vec());
assert_eq!(req.bss_description, bss_description.clone().into());
assert_eq!(req.multiple_bss_candidates, true);
// Send connection response.
let (_stream, ctrl) = txn.expect("connect txn unused")
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl
}
);
let mut connect_result = fake_successful_connect_result();
connect_txn_handle
.send_on_connect_result(&mut connect_result)
.expect("failed to send connection completion");
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// The state machine should have sent a listener update
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks
}))) => {
assert!(networks.is_empty());
}
);
// Check for a connected update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Connected,
status: None,
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure no further updates were sent to listeners
assert_variant!(
exec.run_until_stalled(&mut test_values.update_receiver.into_future()),
Poll::Pending
);
// Three cobalt metrics logged
validate_cobalt_events!(
test_values.cobalt_events,
CONNECTION_ATTEMPT_METRIC_ID,
types::ConnectReason::FidlConnectRequest
);
validate_cobalt_events!(
test_values.cobalt_events,
CONNECTION_ATTEMPT_METRIC_ID,
types::ConnectReason::RetryAfterFailedConnectAttempt
);
}
#[fuchsia::test]
fn connecting_state_fails_to_scan_and_retries() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
// Do test set up manually to get stash server
let (_client_req_sender, client_req_stream) = mpsc::channel(1);
let (update_sender, mut update_receiver) = mpsc::unbounded();
let (sme_proxy, sme_server) =
create_proxy::<fidl_sme::ClientSmeMarker>().expect("failed to create an sme channel");
let sme_req_stream = sme_server.into_stream().expect("could not create SME request stream");
let (saved_networks, mut stash_server) =
exec.run_singlethreaded(SavedNetworksManager::new_and_stash_server());
let saved_networks_manager = Arc::new(saved_networks);
let (cobalt_api, mut cobalt_events) = create_mock_cobalt_sender_and_receiver();
let (persistence_req_sender, _persistence_stream) = create_inspect_persistence_channel();
let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
let telemetry_sender = TelemetrySender::new(telemetry_sender);
let (stats_sender, _stats_receiver) = mpsc::unbounded();
let network_selector = Arc::new(network_selection::NetworkSelector::new(
saved_networks_manager.clone(),
create_mock_cobalt_sender(),
create_wlan_hasher(),
inspect::Inspector::new().root().create_child("network_selector"),
persistence_req_sender,
telemetry_sender.clone(),
));
let next_network_ssid = types::Ssid::try_from("bar").unwrap();
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
credential: Credential::Password("Anything".as_bytes().to_vec()),
scanned: None,
},
reason: types::ConnectReason::FidlConnectRequest,
};
// Store the network in the saved_networks_manager, so we can record connection success
let save_fut = saved_networks_manager.store(
connect_request.target.network.clone().into(),
connect_request.target.credential.clone(),
);
pin_mut!(save_fut);
assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Pending);
process_stash_write(&mut exec, &mut stash_server);
assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Ready(Ok(None)));
let (connect_sender, mut connect_receiver) = oneshot::channel();
let connecting_options = ConnectingOptions {
connect_responder: Some(connect_sender),
connect_request: connect_request.clone(),
attempt_counter: 0,
};
let common_options = CommonStateOptions {
proxy: sme_proxy,
req_stream: client_req_stream.fuse(),
update_sender: update_sender,
saved_networks_manager: saved_networks_manager.clone(),
network_selector,
cobalt_api: cobalt_api,
telemetry_sender,
iface_id: 1,
has_wpa3_support: false,
stats_sender,
};
let initial_state = connecting_state(common_options, connecting_options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check the responder was acknowledged
assert_variant!(exec.run_until_stalled(&mut connect_receiver), Poll::Ready(Ok(())));
// Ensure a scan request is sent to the SME
let sme_fut = sme_req_stream.into_future();
pin_mut!(sme_fut);
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Scan{ txn, .. }) => {
// Send failed scan response.
let (_stream, ctrl) = txn
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl.send_on_error(&mut fidl_sme::ScanError {
code: fidl_sme::ScanErrorCode::InternalError,
message: "Failed to scan".to_string()
})
.expect("failed to send scan error");
}
);
// Check for a connecting update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Connecting,
status: None,
}],
};
assert_variant!(
update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
assert!(exec.wake_next_timer().is_some());
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure a disconnect request is sent to the SME
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::FailedToConnect }) => {
responder.send().expect("could not send sme response");
}
);
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure a new scan request is sent to the SME
let expected_scan_request = fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
ssids: vec![next_network_ssid.to_vec()],
channels: vec![],
});
let bss_description = random_fidl_bss_description!(Wpa2, ssid: next_network_ssid.clone());
let mut scan_results = vec![fidl_sme::ScanResult {
compatible: true,
timestamp_nanos: zx::Time::get_monotonic().into_nanos(),
bss_description: bss_description.clone(),
}];
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Scan {
txn, req, control_handle: _
}) => {
// Validate the request
assert_eq!(req, expected_scan_request);
// Send all the APs
let (_stream, ctrl) = txn
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl.send_on_result(&mut scan_results.iter_mut())
.expect("failed to send scan data");
// Send the end of data
ctrl.send_on_finished()
.expect("failed to send scan data");
}
);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure a connect request is sent to the SME
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
assert_eq!(req.ssid, next_network_ssid.to_vec());
assert_eq!(connect_request.target.credential, req.authentication.credentials.into());
assert_eq!(req.bss_description, bss_description);
assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
assert_eq!(req.multiple_bss_candidates, false);
// Send connection response.
let (_stream, ctrl) = txn.expect("connect txn unused")
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl
.send_on_connect_result(&mut fake_successful_connect_result())
.expect("failed to send connection completion");
}
);
// Cobalt metrics logged
validate_cobalt_events!(
cobalt_events,
CONNECTION_ATTEMPT_METRIC_ID,
types::ConnectReason::FidlConnectRequest
);
validate_cobalt_events!(
cobalt_events,
CONNECTION_ATTEMPT_METRIC_ID,
types::ConnectReason::RetryAfterFailedConnectAttempt
);
}
#[fuchsia::test]
fn connecting_state_fails_to_connect_at_max_retries() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
// Don't use test_values() because of issue with KnownEssStore
let (update_sender, mut update_receiver) = mpsc::unbounded();
let (sme_proxy, sme_server) =
create_proxy::<fidl_sme::ClientSmeMarker>().expect("failed to create an sme channel");
let sme_req_stream = sme_server.into_stream().expect("could not create SME request stream");
let saved_networks_manager = Arc::new(
exec.run_singlethreaded(SavedNetworksManager::new_for_test())
.expect("Failed to create saved networks manager"),
);
let (_client_req_sender, client_req_stream) = mpsc::channel(1);
let (cobalt_api, mut cobalt_events) = create_mock_cobalt_sender_and_receiver();
let (persistence_req_sender, _persistence_stream) = create_inspect_persistence_channel();
let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
let telemetry_sender = TelemetrySender::new(telemetry_sender);
let (stats_sender, _stats_receiver) = mpsc::unbounded();
let network_selector = Arc::new(network_selection::NetworkSelector::new(
saved_networks_manager.clone(),
create_mock_cobalt_sender(),
create_wlan_hasher(),
inspect::Inspector::new().root().create_child("network_selector"),
persistence_req_sender,
telemetry_sender.clone(),
));
let common_options = CommonStateOptions {
proxy: sme_proxy,
req_stream: client_req_stream.fuse(),
update_sender: update_sender,
saved_networks_manager: saved_networks_manager.clone(),
network_selector,
cobalt_api: cobalt_api,
telemetry_sender,
iface_id: 1,
has_wpa3_support: false,
stats_sender,
};
let next_network_ssid = types::Ssid::try_from("bar").unwrap();
let next_security_type = types::SecurityType::None;
let next_credential = Credential::None;
let next_network_identifier = types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: next_security_type,
};
let config_net_id =
network_config::NetworkIdentifier::from(next_network_identifier.clone());
let bss_description = random_fidl_bss_description!(Open, ssid: next_network_ssid.clone());
// save network to check that failed connect is recorded
assert!(exec
.run_singlethreaded(
saved_networks_manager.store(config_net_id.clone(), next_credential.clone()),
)
.expect("Failed to save network")
.is_none());
let before_recording = fasync::Time::now();
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: next_network_identifier,
credential: next_credential,
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Open,
}),
},
reason: types::ConnectReason::FidlConnectRequest,
};
let (connect_sender, mut connect_receiver) = oneshot::channel();
let connecting_options = ConnectingOptions {
connect_responder: Some(connect_sender),
connect_request: connect_request.clone(),
attempt_counter: MAX_CONNECTION_ATTEMPTS - 1,
};
let initial_state = connecting_state(common_options, connecting_options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
let sme_fut = sme_req_stream.into_future();
pin_mut!(sme_fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check the responder was acknowledged
assert_variant!(exec.run_until_stalled(&mut connect_receiver), Poll::Ready(Ok(())));
// Ensure a connect request is sent to the SME
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
assert_eq!(req.ssid, next_network_ssid.to_vec());
assert_eq!(connect_request.target.credential, req.authentication.credentials.into());
assert_eq!(req.bss_description, bss_description.clone());
assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
assert_eq!(req.multiple_bss_candidates, true);
// Send connection response.
let (_stream, ctrl) = txn.expect("connect txn unused")
.into_stream_and_control_handle().expect("error accessing control handle");
let mut connect_result = fidl_sme::ConnectResult {
code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
..fake_successful_connect_result()
};
ctrl
.send_on_connect_result(&mut connect_result)
.expect("failed to send connection completion");
}
);
// After failing to reconnect, the state machine should exit so that the state machine
// monitor can attempt to reconnect the interface.
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
// Check for a connect update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: next_security_type,
},
state: fidl_policy::ConnectionState::Failed,
status: Some(fidl_policy::DisconnectStatus::ConnectionFailed),
}],
};
assert_variant!(
update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Check that failure was recorded in SavedNetworksManager
let mut configs = exec.run_singlethreaded(saved_networks_manager.lookup(&config_net_id));
let network_config = configs.pop().expect("Failed to get saved network");
let mut failures =
network_config.perf_stats.connect_failures.get_recent_for_network(before_recording);
let connect_failure = failures.pop().expect("Saved network is missing failure reason");
assert_eq!(connect_failure.reason, FailureReason::GeneralFailure);
// Cobalt metrics logged
validate_cobalt_events!(
cobalt_events,
CONNECTION_ATTEMPT_METRIC_ID,
types::ConnectReason::FidlConnectRequest
);
}
#[fuchsia::test]
fn connecting_state_fails_to_connect_with_bad_credentials() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
// Don't use test_values() because of issue with KnownEssStore
let (update_sender, mut update_receiver) = mpsc::unbounded();
let (sme_proxy, sme_server) =
create_proxy::<fidl_sme::ClientSmeMarker>().expect("failed to create an sme channel");
let sme_req_stream = sme_server.into_stream().expect("could not create SME request stream");
let saved_networks_manager = Arc::new(
exec.run_singlethreaded(SavedNetworksManager::new_for_test())
.expect("Failed to create saved networks manager"),
);
let (_client_req_sender, client_req_stream) = mpsc::channel(1);
let (cobalt_api, mut cobalt_events) = create_mock_cobalt_sender_and_receiver();
let (persistence_req_sender, _persistence_stream) = create_inspect_persistence_channel();
let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
let telemetry_sender = TelemetrySender::new(telemetry_sender);
let (stats_sender, _stats_receiver) = mpsc::unbounded();
let network_selector = Arc::new(network_selection::NetworkSelector::new(
saved_networks_manager.clone(),
create_mock_cobalt_sender(),
create_wlan_hasher(),
inspect::Inspector::new().root().create_child("network_selector"),
persistence_req_sender,
telemetry_sender.clone(),
));
let common_options = CommonStateOptions {
proxy: sme_proxy,
req_stream: client_req_stream.fuse(),
update_sender: update_sender,
saved_networks_manager: saved_networks_manager.clone(),
network_selector,
cobalt_api: cobalt_api,
telemetry_sender,
iface_id: 1,
has_wpa3_support: false,
stats_sender,
};
let next_network_ssid = types::Ssid::try_from("bar").unwrap();
let next_network_identifier = types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
};
let config_net_id =
network_config::NetworkIdentifier::from(next_network_identifier.clone());
let next_credential = Credential::Password("password".as_bytes().to_vec());
// save network to check that failed connect is recorded
let saved_networks_manager = common_options.saved_networks_manager.clone();
assert!(exec
.run_singlethreaded(
saved_networks_manager.store(config_net_id.clone(), next_credential.clone()),
)
.expect("Failed to save network")
.is_none());
let before_recording = fasync::Time::now();
let bss_description = random_fidl_bss_description!(Wpa2, ssid: next_network_ssid.clone());
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: next_network_identifier,
credential: next_credential,
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal,
}),
},
reason: types::ConnectReason::ProactiveNetworkSwitch,
};
let (connect_sender, mut connect_receiver) = oneshot::channel();
let connecting_options = ConnectingOptions {
connect_responder: Some(connect_sender),
connect_request: connect_request.clone(),
attempt_counter: MAX_CONNECTION_ATTEMPTS - 1,
};
let initial_state = connecting_state(common_options, connecting_options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
let sme_fut = sme_req_stream.into_future();
pin_mut!(sme_fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check the responder was acknowledged
assert_variant!(exec.run_until_stalled(&mut connect_receiver), Poll::Ready(Ok(())));
// Ensure a connect request is sent to the SME
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
assert_eq!(req.ssid, next_network_ssid.to_vec());
assert_eq!(connect_request.target.credential, req.authentication.credentials.into());
assert_eq!(req.bss_description, bss_description.clone());
assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
assert_eq!(req.multiple_bss_candidates, true);
// Send connection response.
let (_stream, ctrl) = txn.expect("connect txn unused")
.into_stream_and_control_handle().expect("error accessing control handle");
let mut connect_result = fidl_sme::ConnectResult {
code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
is_credential_rejected: true,
..fake_successful_connect_result()
};
ctrl
.send_on_connect_result(&mut connect_result)
.expect("failed to send connection completion");
}
);
// The state machine should exit when bad credentials are detected so that the state
// machine monitor can try to connect to another network.
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
// Check for a connect update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Failed,
status: Some(fidl_policy::DisconnectStatus::CredentialsFailed),
}],
};
assert_variant!(
update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Check that failure was recorded in SavedNetworksManager
let mut configs = exec.run_singlethreaded(saved_networks_manager.lookup(&config_net_id));
let network_config = configs.pop().expect("Failed to get saved network");
let mut failures =
network_config.perf_stats.connect_failures.get_recent_for_network(before_recording);
let connect_failure = failures.pop().expect("Saved network is missing failure reason");
assert_eq!(connect_failure.reason, FailureReason::CredentialRejected);
// Cobalt metrics logged
validate_cobalt_events!(
cobalt_events,
CONNECTION_ATTEMPT_METRIC_ID,
types::ConnectReason::ProactiveNetworkSwitch
);
}
#[fuchsia::test]
fn connecting_state_gets_duplicate_connect_request() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
let mut test_values = test_setup();
let next_network_ssid = types::Ssid::try_from("bar").unwrap();
let bss_description = random_fidl_bss_description!(Wpa2, ssid: next_network_ssid.clone());
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
credential: Credential::Password(b"password".to_vec()),
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal,
}),
},
reason: types::ConnectReason::RegulatoryChangeReconnect,
};
let (connect_sender, mut connect_receiver) = oneshot::channel();
let connecting_options = ConnectingOptions {
connect_responder: Some(connect_sender),
connect_request: connect_request.clone(),
attempt_counter: 0,
};
let initial_state = connecting_state(test_values.common_options, connecting_options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
let sme_fut = test_values.sme_req_stream.into_future();
pin_mut!(sme_fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check the responder was acknowledged
assert_variant!(exec.run_until_stalled(&mut connect_receiver), Poll::Ready(Ok(())));
// Check for a connecting update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Connecting,
status: None,
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Send a duplicate connect request
let mut client = Client::new(test_values.client_req_sender);
let (connect_sender2, mut connect_receiver2) = oneshot::channel();
let duplicate_request = types::ConnectRequest {
target: {
types::ConnectionCandidate {
scanned: None, // this incoming request should be deduped regardless of the bss info
..connect_request.clone().target
}
},
// this incoming request should be deduped regardless of the reason
reason: types::ConnectReason::ProactiveNetworkSwitch,
};
client.connect(duplicate_request, connect_sender2).expect("failed to make request");
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check the responder was acknowledged
assert_variant!(exec.run_until_stalled(&mut connect_receiver2), Poll::Ready(Ok(())));
// Ensure a connect request is sent to the SME
let connect_txn_handle = assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
assert_eq!(req.ssid, next_network_ssid.to_vec());
assert_eq!(connect_request.target.credential, req.authentication.credentials.into());
assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
assert_eq!(req.bss_description, bss_description);
assert_eq!(req.multiple_bss_candidates, true);
// Send connection response.
let (_stream, ctrl) = txn.expect("connect txn unused")
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl
}
);
connect_txn_handle
.send_on_connect_result(&mut fake_successful_connect_result())
.expect("failed to send connection completion");
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check for a connect update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Connected,
status: None,
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure no further updates were sent to listeners
assert_variant!(
exec.run_until_stalled(&mut test_values.update_receiver.into_future()),
Poll::Pending
);
// Cobalt metrics logged
validate_cobalt_events!(
test_values.cobalt_events,
CONNECTION_ATTEMPT_METRIC_ID,
types::ConnectReason::RegulatoryChangeReconnect
);
validate_no_cobalt_events!(test_values.cobalt_events);
}
#[fuchsia::test]
fn connecting_state_gets_different_connect_request() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
let mut test_values = test_setup();
let first_network_ssid = types::Ssid::try_from("foo").unwrap();
let second_network_ssid = types::Ssid::try_from("bar").unwrap();
let bss_description = random_fidl_bss_description!(Wpa2, ssid: first_network_ssid.clone());
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: first_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
credential: Credential::Password(b"password".to_vec()),
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal,
}),
},
reason: types::ConnectReason::RegulatoryChangeReconnect,
};
let (connect_sender, mut connect_receiver) = oneshot::channel();
let connecting_options = ConnectingOptions {
connect_responder: Some(connect_sender),
connect_request: connect_request.clone(),
attempt_counter: 0,
};
let initial_state = connecting_state(test_values.common_options, connecting_options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
let sme_fut = test_values.sme_req_stream.into_future();
pin_mut!(sme_fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check the responder was acknowledged
assert_variant!(exec.run_until_stalled(&mut connect_receiver), Poll::Ready(Ok(())));
// Check for a connecting update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: first_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Connecting,
status: None,
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Send a different connect request
let mut client = Client::new(test_values.client_req_sender);
let (connect_sender2, mut connect_receiver2) = oneshot::channel();
let bss_desc2 = random_fidl_bss_description!(Wpa2, ssid: second_network_ssid.clone());
let connect_request2 = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: second_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
credential: Credential::Password(b"password".to_vec()),
scanned: Some(types::ScannedCandidate {
bss_description: bss_desc2.clone(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: false,
security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal,
}),
},
reason: types::ConnectReason::FidlConnectRequest,
};
client.connect(connect_request2.clone(), connect_sender2).expect("failed to make request");
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check for a disconnect update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: first_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Disconnected,
status: Some(fidl_policy::DisconnectStatus::ConnectionStopped),
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// There should be 3 requests to the SME stacked up
// First SME request: connect to the first network
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn: _, control_handle: _ }) => {
assert_eq!(req.ssid, first_network_ssid.to_vec());
assert_eq!(req.bss_description, bss_description.clone());
// Don't bother sending response, listener is gone
}
);
// Second SME request: disconnect
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::FidlConnectRequest }) => {
responder.send().expect("could not send sme response");
}
);
// Progress the state machine
// TODO(fxbug.dev/53505): remove this once the disconnect request is fire-and-forget
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// The state machine should have sent a listener update
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks
}))) => {
assert!(networks.is_empty());
}
);
// Third SME request: connect to the second network
let connect_txn_handle = assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
assert_eq!(req.ssid, second_network_ssid.to_vec());
assert_eq!(req.bss_description, bss_desc2.clone());
assert_eq!(req.multiple_bss_candidates, false);
// Send connection response.
let (_stream, ctrl) = txn.expect("connect txn unused")
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl
}
);
connect_txn_handle
.send_on_connect_result(&mut fake_successful_connect_result())
.expect("failed to send connection completion");
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check the responder was acknowledged
assert_variant!(exec.run_until_stalled(&mut connect_receiver2), Poll::Ready(Ok(())));
// Check for a connecting update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: second_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Connecting,
status: None,
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Check for a connected update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: second_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Connected,
status: None,
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure no further updates were sent to listeners
assert_variant!(
exec.run_until_stalled(&mut test_values.update_receiver.into_future()),
Poll::Pending
);
// Cobalt metrics logged
validate_cobalt_events!(
test_values.cobalt_events,
CONNECTION_ATTEMPT_METRIC_ID,
types::ConnectReason::RegulatoryChangeReconnect
);
validate_cobalt_events!(
test_values.cobalt_events,
CONNECTION_ATTEMPT_METRIC_ID,
types::ConnectReason::FidlConnectRequest
);
}
#[fuchsia::test]
fn connecting_state_gets_disconnect_request() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
let mut test_values = test_setup();
let first_network_ssid = types::Ssid::try_from("foo").unwrap();
let bss_description = random_fidl_bss_description!(Wpa2, ssid: first_network_ssid.clone());
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: first_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
credential: Credential::Password(b"password".to_vec()),
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal,
}),
},
reason: types::ConnectReason::RegulatoryChangeReconnect,
};
let (connect_sender, mut connect_receiver) = oneshot::channel();
let connecting_options = ConnectingOptions {
connect_responder: Some(connect_sender),
connect_request: connect_request.clone(),
attempt_counter: 0,
};
let initial_state = connecting_state(test_values.common_options, connecting_options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
let sme_fut = test_values.sme_req_stream.into_future();
pin_mut!(sme_fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check the responder was acknowledged
assert_variant!(exec.run_until_stalled(&mut connect_receiver), Poll::Ready(Ok(())));
// Check for a connecting update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: first_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Connecting,
status: None,
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Send a disconnect request
let mut client = Client::new(test_values.client_req_sender);
let (disconnect_sender, mut disconnect_receiver) = oneshot::channel();
client
.disconnect(types::DisconnectReason::NetworkUnsaved, disconnect_sender)
.expect("failed to make request");
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check for a disconnect update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: first_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Disconnected,
status: Some(fidl_policy::DisconnectStatus::ConnectionStopped),
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// There should be 2 requests to the SME stacked up
// First SME request: connect to the first network
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn: _, control_handle: _ }) => {
assert_eq!(req.ssid, first_network_ssid.to_vec());
assert_eq!(req.bss_description, bss_description.clone());
// Don't bother sending response, listener is gone
}
);
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::NetworkUnsaved }) => {
responder.send().expect("could not send sme response");
}
);
// The state machine should exit after processing the disconnect.
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
// Check the disconnect responder
assert_variant!(exec.run_until_stalled(&mut disconnect_receiver), Poll::Ready(Ok(())));
// Cobalt metrics logged
validate_cobalt_events!(
test_values.cobalt_events,
CONNECTION_ATTEMPT_METRIC_ID,
types::ConnectReason::RegulatoryChangeReconnect
);
}
#[fuchsia::test]
fn connecting_state_has_broken_sme() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
let test_values = test_setup();
let first_network_ssid = types::Ssid::try_from("foo").unwrap();
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: first_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
credential: Credential::None,
scanned: None,
},
reason: types::ConnectReason::RegulatoryChangeReconnect,
};
let (connect_sender, mut connect_receiver) = oneshot::channel();
let connecting_options = ConnectingOptions {
connect_responder: Some(connect_sender),
connect_request: connect_request.clone(),
attempt_counter: 0,
};
let initial_state = connecting_state(test_values.common_options, connecting_options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
// Break the SME by dropping the server end of the SME stream, so it causes an error
drop(test_values.sme_req_stream);
// Ensure the state machine exits
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
assert!(exec.wake_next_timer().is_some());
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
// Expect the responder to have a success, since the connection was attempted
assert_variant!(exec.run_until_stalled(&mut connect_receiver), Poll::Ready(Ok(())));
}
#[fuchsia::test]
fn connected_state_gets_disconnect_request() {
let mut exec =
fasync::TestExecutor::new_with_fake_time().expect("failed to create an executor");
exec.set_fake_time(fasync::Time::from_nanos(0));
let mut test_values = test_setup();
let mut telemetry_receiver = test_values.telemetry_receiver;
let network_ssid = types::Ssid::try_from("test").unwrap();
let id = types::NetworkIdentifier {
ssid: network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
};
let credential = Credential::Password(b"password".to_vec());
let bss_description = random_bss_description!(Wpa2, ssid: network_ssid.clone());
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: id.clone(),
credential: credential.clone(),
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone().into(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal,
}),
},
reason: types::ConnectReason::RegulatoryChangeReconnect,
};
let (connect_txn_proxy, _connect_txn_stream) =
create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>()
.expect("failed to create a connect txn channel");
let connection_attempt_time = fasync::Time::now();
let time_to_connect = zx::Duration::from_seconds(10);
let options = ConnectedOptions {
currently_fulfilled_request: connect_request,
multiple_bss_candidates: true,
latest_ap_state: Box::new(bss_description.clone()),
connect_txn_stream: connect_txn_proxy.take_event_stream(),
connection_attempt_time,
time_to_connect,
};
let initial_state = connected_state(test_values.common_options, options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
let sme_fut = test_values.sme_req_stream.into_future();
pin_mut!(sme_fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
let disconnect_time = fasync::Time::after(12.hours());
exec.set_fake_time(disconnect_time);
// Send a disconnect request
let mut client = Client::new(test_values.client_req_sender);
let (sender, mut receiver) = oneshot::channel();
client
.disconnect(types::DisconnectReason::FidlStopClientConnectionsRequest, sender)
.expect("failed to make request");
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Respond to the SME disconnect
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::FidlStopClientConnectionsRequest }) => {
responder.send().expect("could not send sme response");
}
);
// Once the disconnect is processed, the state machine should exit.
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
// Check for a disconnect update and the responder
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: id.clone(),
state: fidl_policy::ConnectionState::Disconnected,
status: Some(fidl_policy::DisconnectStatus::ConnectionStopped),
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
assert_variant!(exec.run_until_stalled(&mut receiver), Poll::Ready(Ok(())));
// Cobalt metrics logged
validate_cobalt_events!(
test_values.cobalt_events,
DISCONNECTION_METRIC_ID,
types::DisconnectReason::FidlStopClientConnectionsRequest
);
// Disconnect telemetry event sent
assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => {
assert_variant!(event, TelemetryEvent::Disconnected { track_subsequent_downtime, info } => {
assert!(!track_subsequent_downtime);
assert_eq!(info, DisconnectInfo {
connected_duration: 12.hours(),
is_sme_reconnecting: false,
disconnect_source: fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::FidlStopClientConnectionsRequest),
latest_ap_state: bss_description.clone(),
});
});
});
// The disconnect should have been recorded for the saved network config.
let expected_recorded_connection = ConnectionRecord {
id: id.clone(),
credential: credential.clone(),
data: PastConnectionData {
bssid: bss_description.bssid,
connection_attempt_time,
time_to_connect,
disconnect_time,
connection_uptime: zx::Duration::from_hours(12),
disconnect_reason: types::DisconnectReason::FidlStopClientConnectionsRequest,
signal_data_at_disconnect: bss_selection::SignalData::new(
bss_description.rssi_dbm,
bss_description.snr_db,
bss_selection::EWMA_SMOOTHING_FACTOR,
),
// TODO: record average phy rate over connection once available
average_tx_rate: 0,
},
};
assert_variant!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [connection_data] => {
assert_eq!(connection_data, &expected_recorded_connection);
});
}
#[fuchsia::test]
fn connected_state_records_unexpected_disconnect() {
let mut exec =
fasync::TestExecutor::new_with_fake_time().expect("failed to create an executor");
exec.set_fake_time(fasync::Time::from_nanos(0));
let test_values = test_setup();
let mut telemetry_receiver = test_values.telemetry_receiver;
let network_ssid = types::Ssid::try_from("flaky-network").unwrap();
let security = types::SecurityType::Wpa2;
let credential = Credential::Password(b"password".to_vec());
// Save the network in order to later record the disconnect to it.
let save_fut = test_values.saved_networks_manager.store(
network_config::NetworkIdentifier {
ssid: network_ssid.clone(),
security_type: security.into(),
},
credential.clone(),
);
pin_mut!(save_fut);
assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Ready(Ok(None)));
// Build the values for the connected state.
let bss_description = random_bss_description!(Wpa2, ssid: network_ssid.clone());
let bssid = bss_description.bssid;
let id = types::NetworkIdentifier { ssid: network_ssid, security_type: security };
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: id.clone(),
credential: credential.clone(),
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone().into(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal,
}),
},
reason: types::ConnectReason::RetryAfterFailedConnectAttempt,
};
let (connect_txn_proxy, connect_txn_stream) =
create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>()
.expect("failed to create a connect txn channel");
let connect_txn_handle = connect_txn_stream.control_handle();
let connection_attempt_time = fasync::Time::now();
let time_to_connect = zx::Duration::from_seconds(10);
let options = ConnectedOptions {
currently_fulfilled_request: connect_request.clone(),
multiple_bss_candidates: true,
latest_ap_state: Box::new(bss_description.clone()),
connect_txn_stream: connect_txn_proxy.take_event_stream(),
connection_attempt_time,
time_to_connect,
};
// Start the state machine in the connected state.
let initial_state = connected_state(test_values.common_options, options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
let disconnect_time = fasync::Time::after(12.hours());
exec.set_fake_time(disconnect_time);
// SME notifies Policy of disconnection
let is_sme_reconnecting = false;
let mut fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting);
connect_txn_handle
.send_on_disconnect(&mut fidl_disconnect_info)
.expect("failed to send disconnection event");
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// The disconnect should have been recorded for the saved network config.
let expected_recorded_connection = ConnectionRecord {
id: id.clone(),
credential: credential.clone(),
data: PastConnectionData {
bssid,
connection_attempt_time,
time_to_connect,
disconnect_time,
connection_uptime: zx::Duration::from_hours(12),
disconnect_reason: types::DisconnectReason::DisconnectDetectedFromSme,
signal_data_at_disconnect: bss_selection::SignalData::new(
bss_description.rssi_dbm,
bss_description.snr_db,
bss_selection::EWMA_SMOOTHING_FACTOR,
),
// TODO: record average phy rate over connection once available
average_tx_rate: 0,
},
};
assert_variant!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [connection_data] => {
assert_eq!(connection_data, &expected_recorded_connection);
});
assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => {
// Disconnect telemetry event sent
assert_variant!(event, TelemetryEvent::Disconnected { track_subsequent_downtime, info } => {
assert!(track_subsequent_downtime);
assert_eq!(info, DisconnectInfo {
connected_duration: 12.hours(),
is_sme_reconnecting,
disconnect_source: fidl_disconnect_info.disconnect_source,
latest_ap_state: bss_description,
});
});
});
assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => {
// StartEstablishConnection event sent (because the state machine will attempt
// to reconnect)
assert_variant!(event, TelemetryEvent::StartEstablishConnection { reset_start_time: false });
});
}
#[fuchsia::test]
fn connected_state_reconnect_resets_connected_duration() {
let mut exec =
fasync::TestExecutor::new_with_fake_time().expect("failed to create an executor");
exec.set_fake_time(fasync::Time::from_nanos(0));
let test_values = test_setup();
let mut telemetry_receiver = test_values.telemetry_receiver;
let network_ssid = types::Ssid::try_from("test").unwrap();
let bss_description = random_bss_description!(Wpa2, ssid: network_ssid.clone());
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: network_ssid,
security_type: types::SecurityType::Wpa2,
},
credential: Credential::None,
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone().into(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal,
}),
},
reason: types::ConnectReason::RegulatoryChangeReconnect,
};
let (connect_txn_proxy, connect_txn_stream) =
create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>()
.expect("failed to create a connect txn channel");
let connect_txn_handle = connect_txn_stream.control_handle();
let options = ConnectedOptions {
currently_fulfilled_request: connect_request,
latest_ap_state: Box::new(bss_description),
multiple_bss_candidates: true,
connect_txn_stream: connect_txn_proxy.take_event_stream(),
connection_attempt_time: fasync::Time::now(),
time_to_connect: zx::Duration::from_seconds(10),
};
let initial_state = connected_state(test_values.common_options, options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
exec.set_fake_time(fasync::Time::after(12.hours()));
// SME notifies Policy of disconnection with SME-initiated reconnect
let is_sme_reconnecting = true;
let mut fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting);
connect_txn_handle
.send_on_disconnect(&mut fidl_disconnect_info)
.expect("failed to send disconnection event");
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Disconnect telemetry event sent
assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => {
assert_variant!(event, TelemetryEvent::Disconnected { info, .. } => {
assert_eq!(info.connected_duration, 12.hours());
});
});
// SME notifies Policy of reconnection successful
exec.set_fake_time(fasync::Time::after(1.second()));
let mut connect_result =
fidl_sme::ConnectResult { is_reconnect: true, ..fake_successful_connect_result() };
connect_txn_handle
.send_on_connect_result(&mut connect_result)
.expect("failed to send connect result event");
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
assert_variant!(
telemetry_receiver.try_next(),
Ok(Some(TelemetryEvent::ConnectResult { .. }))
);
// SME notifies Policy of another disconnection
exec.set_fake_time(fasync::Time::after(2.hours()));
let is_sme_reconnecting = false;
let mut fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting);
connect_txn_handle
.send_on_disconnect(&mut fidl_disconnect_info)
.expect("failed to send disconnection event");
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Another disconnect telemetry event sent
assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => {
assert_variant!(event, TelemetryEvent::Disconnected { info, .. } => {
assert_eq!(info.connected_duration, 2.hours());
});
});
}
#[fuchsia::test]
fn connected_state_records_unexpected_disconnect_unspecified_bss() {
let mut exec =
fasync::TestExecutor::new_with_fake_time().expect("failed to create an executor");
let connection_attempt_time = fasync::Time::from_nanos(0);
exec.set_fake_time(connection_attempt_time);
let test_values = test_setup();
let network_ssid = types::Ssid::try_from("flaky-network").unwrap();
let security = types::SecurityType::Wpa2;
let credential = Credential::Password(b"password".to_vec());
let id = network_config::NetworkIdentifier {
ssid: network_ssid.clone(),
security_type: security.into(),
};
// Setup for network selection in the connecting state to select the intended network.
let expected_config =
network_config::NetworkConfig::new(id.clone().into(), credential.clone(), false)
.expect("failed to create network config");
test_values.saved_networks_manager.set_lookup_compatible_response(vec![expected_config]);
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: id.clone(),
credential: credential.clone(),
scanned: None,
},
reason: types::ConnectReason::RetryAfterFailedConnectAttempt,
};
let (connect_sender, _connect_receiver) = oneshot::channel();
let connecting_options = ConnectingOptions {
connect_responder: Some(connect_sender),
connect_request: connect_request.clone(),
attempt_counter: 0,
};
let initial_state = connecting_state(test_values.common_options, connecting_options);
let state_fut = run_state_machine(initial_state);
pin_mut!(state_fut);
let sme_fut = test_values.sme_req_stream.into_future();
pin_mut!(sme_fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut state_fut), Poll::Pending);
// Send a scan for the requested network
let bss_description = random_fidl_bss_description!(Wpa2, ssid: network_ssid.clone());
let mut scan_results = vec![fidl_sme::ScanResult {
compatible: true,
timestamp_nanos: zx::Time::get_monotonic().into_nanos(),
bss_description: bss_description.clone(),
}];
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Scan {
txn, req: _, control_handle: _
}) => {
// Send the scan results up
let (_stream, ctrl) = txn
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl.send_on_result(&mut scan_results.iter_mut())
.expect("failed to send scan data");
// Send the end of data
ctrl.send_on_finished()
.expect("failed to send scan data");
}
);
assert_variant!(exec.run_until_stalled(&mut state_fut), Poll::Pending);
let time_to_connect = 10.seconds();
exec.set_fake_time(fasync::Time::after(time_to_connect));
// Process connect request sent to SME
let connect_txn_handle = assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req: _, txn, control_handle: _ }) => {
// Send connection response.
let (_stream, ctrl) = txn.expect("connect txn unused")
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl
}
);
connect_txn_handle
.send_on_connect_result(&mut fake_successful_connect_result())
.expect("failed to send connection completion");
assert_variant!(exec.run_until_stalled(&mut state_fut), Poll::Pending);
// SME notifies Policy of disconnection.
let disconnect_time = fasync::Time::after(5.hours());
exec.set_fake_time(disconnect_time);
let is_sme_reconnecting = false;
connect_txn_handle
.send_on_disconnect(&mut generate_disconnect_info(is_sme_reconnecting))
.expect("failed to send disconnection event");
assert_variant!(exec.run_until_stalled(&mut state_fut), Poll::Pending);
// The connection data should have been recorded at disconnect.
let expected_recorded_connection = ConnectionRecord {
id: id.clone().into(),
credential: credential.clone(),
data: PastConnectionData {
bssid: types::Bssid(bss_description.bssid),
connection_attempt_time,
time_to_connect,
disconnect_time,
connection_uptime: zx::Duration::from_hours(5),
disconnect_reason: types::DisconnectReason::DisconnectDetectedFromSme,
signal_data_at_disconnect: bss_selection::SignalData::new(
bss_description.rssi_dbm,
bss_description.snr_db,
bss_selection::EWMA_SMOOTHING_FACTOR,
),
average_tx_rate: 0,
},
};
assert_variant!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [connection_data] => {
assert_eq!(connection_data, &expected_recorded_connection);
});
}
#[fuchsia::test]
fn connected_state_gets_duplicate_connect_request() {
let mut exec =
fasync::TestExecutor::new_with_fake_time().expect("failed to create an executor");
exec.set_fake_time(fasync::Time::from_nanos(0));
let mut test_values = test_setup();
let mut telemetry_receiver = test_values.telemetry_receiver;
let network_ssid = types::Ssid::try_from("test").unwrap();
let bss_description = random_bss_description!(Wpa2, ssid: network_ssid.clone());
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
credential: Credential::None,
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone().into(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal,
}),
},
reason: types::ConnectReason::RegulatoryChangeReconnect,
};
let (connect_txn_proxy, _connect_txn_stream) =
create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>()
.expect("failed to create a connect txn channel");
let options = ConnectedOptions {
currently_fulfilled_request: connect_request.clone(),
latest_ap_state: Box::new(bss_description),
multiple_bss_candidates: true,
connect_txn_stream: connect_txn_proxy.take_event_stream(),
connection_attempt_time: fasync::Time::now(),
time_to_connect: zx::Duration::from_seconds(10),
};
let initial_state = connected_state(test_values.common_options, options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
let sme_fut = test_values.sme_req_stream.into_future();
pin_mut!(sme_fut);
// Send another duplicate request
let mut client = Client::new(test_values.client_req_sender);
let (sender, mut receiver) = oneshot::channel();
client.connect(connect_request.clone(), sender).expect("failed to make request");
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure nothing was sent to the SME
assert_variant!(poll_sme_req(&mut exec, &mut sme_fut), Poll::Pending);
// Check the responder was acknowledged
assert_variant!(exec.run_until_stalled(&mut receiver), Poll::Ready(Ok(())));
// No cobalt metrics logged
validate_no_cobalt_events!(test_values.cobalt_events);
// No telemetry event is sent
assert_variant!(telemetry_receiver.try_next(), Err(_));
}
#[fuchsia::test]
fn connected_state_gets_different_connect_request() {
let mut exec =
fasync::TestExecutor::new_with_fake_time().expect("failed to create an executor");
exec.set_fake_time(fasync::Time::from_nanos(0));
let mut test_values = test_setup();
let mut telemetry_receiver = test_values.telemetry_receiver;
let first_network_ssid = types::Ssid::try_from("foo").unwrap();
let second_network_ssid = types::Ssid::try_from("bar").unwrap();
let bss_description = random_bss_description!(Wpa2, ssid: first_network_ssid.clone());
let id_1 = types::NetworkIdentifier {
ssid: first_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
};
let credential_1 = Credential::Password(b"some-password".to_vec());
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: id_1.clone(),
credential: credential_1.clone(),
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone().into(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal,
}),
},
reason: types::ConnectReason::IdleInterfaceAutoconnect,
};
let (connect_txn_proxy, _connect_txn_stream) =
create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>()
.expect("failed to create a connect txn channel");
let connection_attempt_time = fasync::Time::now();
let time_to_connect = zx::Duration::from_seconds(10);
let options = ConnectedOptions {
currently_fulfilled_request: connect_request.clone(),
latest_ap_state: Box::new(bss_description.clone()),
multiple_bss_candidates: true,
connect_txn_stream: connect_txn_proxy.take_event_stream(),
connection_attempt_time,
time_to_connect,
};
let initial_state = connected_state(test_values.common_options, options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
let sme_fut = test_values.sme_req_stream.into_future();
pin_mut!(sme_fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
let disconnect_time = fasync::Time::after(12.hours());
exec.set_fake_time(disconnect_time);
// Send a different connect request
let second_bss_desc = random_fidl_bss_description!(Wpa2, ssid: second_network_ssid.clone());
let mut client = Client::new(test_values.client_req_sender);
let (connect_sender2, mut connect_receiver2) = oneshot::channel();
let connect_request2 = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: second_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
credential: Credential::Password(b"password".to_vec()),
scanned: Some(types::ScannedCandidate {
bss_description: second_bss_desc.clone().into(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal,
}),
},
reason: types::ConnectReason::ProactiveNetworkSwitch,
};
client.connect(connect_request2.clone(), connect_sender2).expect("failed to make request");
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// There should be 2 requests to the SME stacked up
// First SME request: disconnect
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch }) => {
responder.send().expect("could not send sme response");
}
);
// Progress the state machine
// TODO(fxbug.dev/53505): remove this once the disconnect request is fire-and-forget
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Second SME request: connect to the second network
let connect_txn_handle = assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
assert_eq!(req.ssid, second_network_ssid.to_vec());
assert_eq!(req.bss_description, second_bss_desc.clone());
// Send connection response.
let (_stream, ctrl) = txn.expect("connect txn unused")
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl
}
);
connect_txn_handle
.send_on_connect_result(&mut fake_successful_connect_result())
.expect("failed to send connection completion");
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check for a disconnect update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: id_1.clone(),
state: fidl_policy::ConnectionState::Disconnected,
status: Some(fidl_policy::DisconnectStatus::ConnectionStopped),
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Disconnect telemetry event sent
assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => {
assert_variant!(event, TelemetryEvent::Disconnected { track_subsequent_downtime, info } => {
assert!(!track_subsequent_downtime);
assert_eq!(info, DisconnectInfo {
connected_duration: 12.hours(),
is_sme_reconnecting: false,
disconnect_source: fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch),
latest_ap_state: bss_description.clone(),
});
});
});
// Check the responder was acknowledged
assert_variant!(exec.run_until_stalled(&mut connect_receiver2), Poll::Ready(Ok(())));
// Check for a connecting update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: second_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Connecting,
status: None,
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Check for a connected update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: second_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Connected,
status: None,
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure no further updates were sent to listeners
assert_variant!(
exec.run_until_stalled(&mut test_values.update_receiver.into_future()),
Poll::Pending
);
// Cobalt metrics logged
validate_cobalt_events!(
test_values.cobalt_events,
DISCONNECTION_METRIC_ID,
types::DisconnectReason::ProactiveNetworkSwitch
);
validate_cobalt_events!(
test_values.cobalt_events,
CONNECTION_ATTEMPT_METRIC_ID,
types::ConnectReason::ProactiveNetworkSwitch
);
// Check that the first connection was recorded
let expected_recorded_connection = ConnectionRecord {
id: id_1.clone(),
credential: credential_1.clone(),
data: PastConnectionData {
bssid: bss_description.bssid,
connection_attempt_time,
time_to_connect,
disconnect_time,
connection_uptime: zx::Duration::from_hours(12),
disconnect_reason: types::DisconnectReason::ProactiveNetworkSwitch,
signal_data_at_disconnect: bss_selection::SignalData::new(
bss_description.rssi_dbm,
bss_description.snr_db,
bss_selection::EWMA_SMOOTHING_FACTOR,
),
// TODO: record average phy rate over connection once available
average_tx_rate: 0,
},
};
assert_variant!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [connection_data] => {
assert_eq!(connection_data, &expected_recorded_connection);
});
}
#[fuchsia::test]
fn connected_state_notified_of_network_disconnect_no_sme_reconnect() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
// Do test set up manually to get stash server
let (_client_req_sender, client_req_stream) = mpsc::channel(1);
let (update_sender, mut update_receiver) = mpsc::unbounded();
let (sme_proxy, sme_server) =
create_proxy::<fidl_sme::ClientSmeMarker>().expect("failed to create an sme channel");
let sme_req_stream = sme_server.into_stream().expect("could not create SME request stream");
let (saved_networks, mut stash_server) =
exec.run_singlethreaded(SavedNetworksManager::new_and_stash_server());
let saved_networks_manager = Arc::new(saved_networks);
let (cobalt_api, mut cobalt_events) = create_mock_cobalt_sender_and_receiver();
let (persistence_req_sender, _persistence_stream) = create_inspect_persistence_channel();
let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
let telemetry_sender = TelemetrySender::new(telemetry_sender);
let (stats_sender, _stats_receiver) = mpsc::unbounded();
let network_selector = Arc::new(network_selection::NetworkSelector::new(
saved_networks_manager.clone(),
create_mock_cobalt_sender(),
create_wlan_hasher(),
inspect::Inspector::new().root().create_child("network_selector"),
persistence_req_sender,
telemetry_sender.clone(),
));
let network_ssid = types::Ssid::try_from("foo").unwrap();
let bss_description = random_bss_description!(Wpa2, ssid: network_ssid.clone());
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
credential: Credential::Password("Anything".as_bytes().to_vec()),
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone().into(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal,
}),
},
reason: types::ConnectReason::RegulatoryChangeReconnect,
};
// Store the network in the saved_networks_manager, so we can record connection success
let save_fut = saved_networks_manager.store(
connect_request.target.network.clone().into(),
connect_request.target.credential.clone(),
);
pin_mut!(save_fut);
assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Pending);
process_stash_write(&mut exec, &mut stash_server);
assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Ready(Ok(None)));
let common_options = CommonStateOptions {
proxy: sme_proxy,
req_stream: client_req_stream.fuse(),
update_sender: update_sender,
saved_networks_manager: saved_networks_manager.clone(),
network_selector,
cobalt_api: cobalt_api,
telemetry_sender,
iface_id: 1,
has_wpa3_support: false,
stats_sender,
};
let (connect_txn_proxy, connect_txn_stream) =
create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>()
.expect("failed to create a connect txn channel");
let connect_txn_handle = connect_txn_stream.control_handle();
let options = ConnectedOptions {
currently_fulfilled_request: connect_request.clone(),
latest_ap_state: Box::new(bss_description),
multiple_bss_candidates: true,
connect_txn_stream: connect_txn_proxy.take_event_stream(),
connection_attempt_time: fasync::Time::now(),
time_to_connect: zx::Duration::from_seconds(10),
};
let initial_state = connected_state(common_options, options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
let sme_fut = sme_req_stream.into_future();
pin_mut!(sme_fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// SME notifies Policy of disconnection.
let is_sme_reconnecting = false;
connect_txn_handle
.send_on_disconnect(&mut generate_disconnect_info(is_sme_reconnecting))
.expect("failed to send disconnection event");
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check for an SME disconnect request
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::DisconnectDetectedFromSme }) => {
responder.send().expect("could not send sme response");
}
);
// Progress the state machine
// TODO(fxbug.dev/53505): remove this once the disconnect request is fire-and-forget
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check for a disconnect update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Disconnected,
status: Some(fidl_policy::DisconnectStatus::ConnectionFailed),
}],
};
assert_variant!(
update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Check for a scan to find a new BSS to reconnect with
let new_bss_desc = random_fidl_bss_description!(Wpa2, ssid: network_ssid.clone());
let expected_scan_request = fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
ssids: vec![network_ssid.to_vec()],
channels: vec![],
});
let mut scan_results = vec![fidl_sme::ScanResult {
compatible: true,
timestamp_nanos: zx::Time::get_monotonic().into_nanos(),
bss_description: new_bss_desc.clone(),
}];
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Scan {
txn, req, control_handle: _
}) => {
// Validate the request
assert_eq!(req, expected_scan_request);
// Send all the APs
let (_stream, ctrl) = txn
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl.send_on_result(&mut scan_results.iter_mut())
.expect("failed to send scan data");
// Send the end of data
ctrl.send_on_finished()
.expect("failed to send scan data");
}
);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check for an SME request to reconnect
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
assert_eq!(req.ssid, network_ssid.to_vec());
assert_eq!(req.bss_description, new_bss_desc.clone());
assert_eq!(req.multiple_bss_candidates, false);
// Send connection response.
let (_stream, ctrl) = txn.expect("connect txn unused")
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl
.send_on_connect_result(&mut fake_successful_connect_result())
.expect("failed to send connection completion");
}
);
// Check for a connecting update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Connecting,
status: None,
}],
};
assert_variant!(
update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Cobalt metrics logged
validate_cobalt_events!(
cobalt_events,
DISCONNECTION_METRIC_ID,
types::DisconnectReason::DisconnectDetectedFromSme
);
validate_cobalt_events!(
cobalt_events,
CONNECTION_ATTEMPT_METRIC_ID,
types::ConnectReason::RetryAfterDisconnectDetected
);
}
#[fuchsia::test]
fn connected_state_notified_of_network_disconnect_sme_reconnect_successfully() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
let mut test_values = test_setup();
let network_ssid = types::Ssid::try_from("foo").unwrap();
let bss_description = random_bss_description!(Wpa2, ssid: network_ssid.clone());
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
credential: Credential::None,
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone().into(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal,
}),
},
reason: types::ConnectReason::IdleInterfaceAutoconnect,
};
let (connect_txn_proxy, connect_txn_stream) =
create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>()
.expect("failed to create a connect txn channel");
let connect_txn_handle = connect_txn_stream.control_handle();
let options = ConnectedOptions {
currently_fulfilled_request: connect_request.clone(),
latest_ap_state: Box::new(bss_description),
multiple_bss_candidates: true,
connect_txn_stream: connect_txn_proxy.take_event_stream(),
connection_attempt_time: fasync::Time::now(),
time_to_connect: zx::Duration::from_seconds(10),
};
let initial_state = connected_state(test_values.common_options, options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// SME notifies Policy of disconnection
let is_sme_reconnecting = true;
connect_txn_handle
.send_on_disconnect(&mut generate_disconnect_info(is_sme_reconnecting))
.expect("failed to send disconnection event");
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// SME notifies Policy that reconnects succeeds
let mut connect_result =
fidl_sme::ConnectResult { is_reconnect: true, ..fake_successful_connect_result() };
connect_txn_handle
.send_on_connect_result(&mut connect_result)
.expect("failed to send reconnection result");
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check there were no state updates
assert_variant!(test_values.update_receiver.try_next(), Err(_));
// Cobalt metrics logged
validate_cobalt_events!(
test_values.cobalt_events,
DISCONNECTION_METRIC_ID,
types::DisconnectReason::DisconnectDetectedFromSme
);
validate_no_cobalt_events!(test_values.cobalt_events);
}
#[fuchsia::test]
fn connected_state_notified_of_network_disconnect_sme_reconnect_unsuccessfully() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
let mut test_values = test_setup();
let (saved_networks, mut stash_server) =
exec.run_singlethreaded(SavedNetworksManager::new_and_stash_server());
let saved_networks_manager = Arc::new(saved_networks);
test_values.common_options.saved_networks_manager = saved_networks_manager.clone();
let (persistence_req_sender, _persistence_stream) = create_inspect_persistence_channel();
let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
let network_selector = Arc::new(network_selection::NetworkSelector::new(
saved_networks_manager.clone(),
create_mock_cobalt_sender(),
create_wlan_hasher(),
inspect::Inspector::new().root().create_child("network_selector"),
persistence_req_sender,
TelemetrySender::new(telemetry_sender),
));
test_values.common_options.network_selector = network_selector;
let network_ssid = types::Ssid::try_from("foo").unwrap();
let bss_description = random_bss_description!(Wpa2, ssid: network_ssid.clone());
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
credential: Credential::Password("Anything".as_bytes().to_vec()),
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone().into(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal,
}),
},
reason: types::ConnectReason::IdleInterfaceAutoconnect,
};
// Store the network in the saved_networks_manager, so we can record connection success
let save_fut = saved_networks_manager.store(
connect_request.target.network.clone().into(),
connect_request.target.credential.clone(),
);
pin_mut!(save_fut);
assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Pending);
process_stash_write(&mut exec, &mut stash_server);
assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Ready(Ok(None)));
let (connect_txn_proxy, connect_txn_stream) =
create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>()
.expect("failed to create a connect txn channel");
let mut connect_txn_handle = connect_txn_stream.control_handle();
let options = ConnectedOptions {
currently_fulfilled_request: connect_request.clone(),
latest_ap_state: Box::new(bss_description),
multiple_bss_candidates: true,
connect_txn_stream: connect_txn_proxy.take_event_stream(),
connection_attempt_time: fasync::Time::now(),
time_to_connect: zx::Duration::from_seconds(10),
};
let initial_state = connected_state(test_values.common_options, options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
let sme_fut = test_values.sme_req_stream.into_future();
pin_mut!(sme_fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// SME notifies Policy of disconnection
let is_sme_reconnecting = true;
connect_txn_handle
.send_on_disconnect(&mut generate_disconnect_info(is_sme_reconnecting))
.expect("failed to send disconnection event");
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// SME notifies Policy that reconnects fails
let mut connect_result = fidl_sme::ConnectResult {
code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
is_reconnect: true,
..fake_successful_connect_result()
};
connect_txn_handle
.send_on_connect_result(&mut connect_result)
.expect("failed to send reconnection result");
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check for an SME disconnect request
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::DisconnectDetectedFromSme }) => {
responder.send().expect("could not send sme response");
}
);
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check for a disconnect update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Disconnected,
status: Some(fidl_policy::DisconnectStatus::ConnectionFailed),
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Check for a scan to find a new BSS to reconnect with
let new_bss_description = random_fidl_bss_description!(Wpa2, ssid: network_ssid.clone());
let expected_scan_request = fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
ssids: vec![network_ssid.to_vec()],
channels: vec![],
});
let mut scan_results = vec![fidl_sme::ScanResult {
compatible: true,
timestamp_nanos: zx::Time::get_monotonic().into_nanos(),
bss_description: new_bss_description.clone(),
}];
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Scan {
txn, req, control_handle: _
}) => {
// Validate the request
assert_eq!(req, expected_scan_request);
// Send all the APs
let (_stream, ctrl) = txn
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl.send_on_result(&mut scan_results.iter_mut())
.expect("failed to send scan data");
// Send the end of data
ctrl.send_on_finished()
.expect("failed to send scan data");
}
);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check for an SME request to reconnect
connect_txn_handle = assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
assert_eq!(req.ssid, network_ssid.to_vec());
assert_eq!(req.bss_description, new_bss_description.clone());
assert_eq!(req.multiple_bss_candidates, false);
// Send connection response.
let (_stream, ctrl) = txn.expect("connect txn unused")
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl
}
);
connect_txn_handle
.send_on_connect_result(&mut fake_successful_connect_result())
.expect("failed to send connection completion");
// Check for a connecting update
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Connecting,
status: None,
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
// Cobalt metrics logged
validate_cobalt_events!(
test_values.cobalt_events,
DISCONNECTION_METRIC_ID,
types::DisconnectReason::DisconnectDetectedFromSme
);
validate_cobalt_events!(
test_values.cobalt_events,
CONNECTION_ATTEMPT_METRIC_ID,
types::ConnectReason::RetryAfterDisconnectDetected
);
}
#[fuchsia::test]
fn connected_state_on_signal_report() {
let mut exec =
fasync::TestExecutor::new_with_fake_time().expect("failed to create an executor");
exec.set_fake_time(fasync::Time::from_nanos(0));
let mut test_values = test_setup();
let mut telemetry_receiver = test_values.telemetry_receiver;
let network_ssid = types::Ssid::try_from("test").unwrap();
let init_rssi = -40;
let init_snr = 30;
let bss_description = random_bss_description!(Wpa2, ssid: network_ssid.clone(), rssi_dbm: init_rssi, snr_db: init_snr);
// Add a PastConnectionData for the connected network to be send in BSS quality data.
let mut past_connections = PastConnectionList::new();
let mut past_connection_data = random_connection_data();
past_connection_data.bssid = bss_description.bssid;
past_connections.add(past_connection_data);
let mut saved_networks_manager = FakeSavedNetworksManager::new();
saved_networks_manager.past_connections_response = past_connections.clone();
test_values.common_options.saved_networks_manager = Arc::new(saved_networks_manager);
// Set up the state machine, starting at the connected state.
let (initial_state, connect_txn_stream) =
connected_state_setup(test_values.common_options, bss_description.clone());
let connect_txn_handle = connect_txn_stream.control_handle();
let fut = run_state_machine(initial_state);
pin_mut!(fut);
let sme_fut = test_values.sme_req_stream.into_future();
pin_mut!(sme_fut);
// Send the first signal report from SME
let rssi_1 = -50;
let snr_1 = 25;
let mut fidl_signal_report =
fidl_internal::SignalReportIndication { rssi_dbm: rssi_1, snr_db: snr_1 };
connect_txn_handle
.send_on_signal_report(&mut fidl_signal_report)
.expect("failed to send signal report");
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Verify telemetry event for signal report data then RSSI data.
assert_variant!(telemetry_receiver.try_next(), Ok(Some(TelemetryEvent::OnSignalReport {ind, rssi_velocity})) => {
assert_eq!(ind, fidl_signal_report);
// verify that RSSI velocity is negative since the signal report RSSI is lower.
assert_lt!(rssi_velocity, 0);
});
// Do a quick check that state machine does not exist and there's no disconnect to SME
assert_variant!(poll_sme_req(&mut exec, &mut sme_fut), Poll::Pending);
// Verify that connection stats are sent out
let id = types::NetworkIdentifier {
ssid: network_ssid,
security_type: types::SecurityType::Wpa2,
};
let stats = test_values
.stats_receiver
.try_next()
.expect("failed to get connection stats")
.expect("next connection stats is missing");
// Test setup always use iface ID 1.
assert_eq!(stats.iface_id, 1);
assert_eq!(stats.id, id);
// EWMA RSSI and SNR should be between the initial and the newest values.
let ewma_rssi_1 = stats.quality_data.signal_data.ewma_rssi;
assert_lt!(ewma_rssi_1.get(), init_rssi);
assert_gt!(ewma_rssi_1.get(), rssi_1);
let ewma_snr_1 = stats.quality_data.signal_data.ewma_snr;
assert_lt!(ewma_snr_1.get(), init_snr);
assert_gt!(ewma_snr_1.get(), snr_1);
// Check that RSSI velocity is negative.
let rssi_velocity_1 = stats.quality_data.signal_data.rssi_velocity;
assert_lt!(rssi_velocity_1, 0);
// Check that the BssQualityData includes the past connection data.
assert_eq!(stats.quality_data.past_connections_list, past_connections.clone());
// Check that the channel is included.
assert_eq!(stats.quality_data.channel, bss_description.channel);
// Send a second signal report with higher RSSI and SNR than the previous reports.
let rssi_1 = -30;
let snr_1 = 35;
let mut fidl_signal_report =
fidl_internal::SignalReportIndication { rssi_dbm: rssi_1, snr_db: snr_1 };
connect_txn_handle
.send_on_signal_report(&mut fidl_signal_report)
.expect("failed to send signal report");
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Verify that a telemetry event is sent
assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => {
assert_variant!(event, TelemetryEvent::OnSignalReport { ind, rssi_velocity } => {
assert_eq!(ind, fidl_signal_report);
// Velocity should be greater than previous one since RSSI is higher,
assert_gt!(rssi_velocity, rssi_velocity_1);
});
});
// Verify that the new EWMA values are higher than the previous values.
let stats = test_values
.stats_receiver
.try_next()
.expect("failed to get connection stats")
.expect("next connection stats is missing");
assert_eq!(stats.iface_id, 1);
assert_eq!(stats.id, id);
// Check that EWMA RSSI and SNR values are greater than the previous values.
assert_gt!(stats.quality_data.signal_data.ewma_rssi.get(), ewma_rssi_1.get());
assert_gt!(stats.quality_data.signal_data.ewma_snr.get(), ewma_snr_1.get());
// Check that RSSI velocity is greater than the previous velocity.
assert_gt!(stats.quality_data.signal_data.rssi_velocity, rssi_velocity_1);
// Check that the BssQualityData includes the past connection data.
assert_eq!(stats.quality_data.past_connections_list, past_connections);
// Check that the channel is included.
assert_eq!(stats.quality_data.channel, bss_description.channel);
}
#[fuchsia::test]
fn connected_state_should_roam() {
let mut exec =
fasync::TestExecutor::new_with_fake_time().expect("failed to create an executor");
exec.set_fake_time(fasync::Time::from_nanos(0));
let test_values = test_setup();
let mut telemetry_receiver = test_values.telemetry_receiver;
let network_ssid = types::Ssid::try_from("test").unwrap();
let init_rssi = -75;
let init_snr = 30;
let bss_description = random_bss_description!(
Wpa2,
ssid: network_ssid.clone(),
rssi_dbm: init_rssi,
snr_db: init_snr,
);
// Set up the state machine, starting at the connected state.
let (initial_state, connect_txn_stream) =
connected_state_setup(test_values.common_options, bss_description.clone());
let connect_txn_handle = connect_txn_stream.control_handle();
let fut = run_state_machine(initial_state);
pin_mut!(fut);
// Send a signal report indicating the connection is weak.
let rssi_1 = -90;
let snr_1 = 25;
let mut fidl_signal_report =
fidl_internal::SignalReportIndication { rssi_dbm: rssi_1, snr_db: snr_1 };
connect_txn_handle
.send_on_signal_report(&mut fidl_signal_report)
.expect("failed to send signal report");
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Verify that telemetry events are sent for the signal report and a Roam Scan.
assert_variant!(
telemetry_receiver.try_next(),
Ok(Some(TelemetryEvent::OnSignalReport {ind, rssi_velocity})) => {
assert_eq!(ind, fidl_signal_report);
// verify that RSSI velocity is negative since the signal report RSSI is lower.
assert_lt!(rssi_velocity, 0);
}
);
assert_variant!(telemetry_receiver.try_next(), Ok(Some(TelemetryEvent::RoamingScan {})));
}
#[fuchsia::test]
fn connected_state_on_channel_switched() {
let mut exec =
fasync::TestExecutor::new_with_fake_time().expect("failed to create an executor");
exec.set_fake_time(fasync::Time::from_nanos(0));
let test_values = test_setup();
let mut telemetry_receiver = test_values.telemetry_receiver;
let network_ssid = types::Ssid::try_from("test").unwrap();
let bss_description = random_bss_description!(Wpa2, ssid: network_ssid.clone());
let (initial_state, connect_txn_stream) =
connected_state_setup(test_values.common_options, bss_description);
let connect_txn_handle = connect_txn_stream.control_handle();
let fut = run_state_machine(initial_state);
pin_mut!(fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
let mut channel_switch_info = fidl_internal::ChannelSwitchInfo { new_channel: 10 };
connect_txn_handle
.send_on_channel_switched(&mut channel_switch_info)
.expect("failed to send signal report");
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Verify telemetry event
assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => {
assert_variant!(event, TelemetryEvent::OnChannelSwitched { info } => {
assert_eq!(info, channel_switch_info);
});
});
// Have SME notify Policy of disconnection so we can see whether the channel in the
// BssDescription has changed.
let is_sme_reconnecting = false;
let mut fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting);
connect_txn_handle
.send_on_disconnect(&mut fidl_disconnect_info)
.expect("failed to send disconnection event");
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Verify telemetry event
assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => {
assert_variant!(event, TelemetryEvent::Disconnected { info, .. } => {
assert_eq!(info.latest_ap_state.channel.primary, 10);
});
});
}
// Set up connected state, returning its fut, connect_txn_stream, and a view into
// BssDescription held by the connected state
fn connected_state_setup(
common_options: CommonStateOptions,
bss_description: BssDescription,
) -> (impl Future<Output = Result<State, ExitReason>>, fidl_sme::ConnectTransactionRequestStream)
{
let protection = bss_description.protection();
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: bss_description.ssid.clone(),
security_type: match protection {
Protection::Open => types::SecurityType::None,
Protection::Wep => types::SecurityType::Wep,
Protection::Wpa1 => types::SecurityType::Wpa,
Protection::Wpa2Personal => types::SecurityType::Wpa2,
Protection::Wpa3Personal => types::SecurityType::Wpa3,
_ => panic!("unsupported BssDescription protection type for unit tests."),
},
},
credential: Credential::None,
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone().into(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: protection.into(),
}),
},
reason: types::ConnectReason::RegulatoryChangeReconnect,
};
let (connect_txn_proxy, connect_txn_stream) =
create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>()
.expect("failed to create a connect txn channel");
let options = ConnectedOptions {
currently_fulfilled_request: connect_request,
latest_ap_state: Box::new(bss_description.clone()),
multiple_bss_candidates: true,
connect_txn_stream: connect_txn_proxy.take_event_stream(),
connection_attempt_time: fasync::Time::now(),
time_to_connect: zx::Duration::from_seconds(10),
};
let initial_state = connected_state(common_options, options);
(initial_state, connect_txn_stream)
}
#[fuchsia::test]
fn disconnecting_state_completes_and_exits() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
let mut test_values = test_setup();
let (sender, mut receiver) = oneshot::channel();
let disconnecting_options = DisconnectingOptions {
disconnect_responder: Some(sender),
previous_network: None,
next_network: None,
reason: types::DisconnectReason::RegulatoryRegionChange,
};
let initial_state = disconnecting_state(test_values.common_options, disconnecting_options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
let sme_fut = test_values.sme_req_stream.into_future();
pin_mut!(sme_fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure a disconnect request is sent to the SME
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::RegulatoryRegionChange }) => {
responder.send().expect("could not send sme response");
}
);
// Ensure the state machine exits once the disconnect is processed.
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
// The state machine should have sent a listener update
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks
}))) => {
assert!(networks.is_empty());
}
);
// Expect the responder to be acknowledged
assert_variant!(exec.run_until_stalled(&mut receiver), Poll::Ready(Ok(())));
}
#[fuchsia::test]
fn disconnecting_state_completes_disconnect_to_connecting() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
let mut test_values = test_setup();
let previous_network_ssid = types::Ssid::try_from("foo").unwrap();
let next_network_ssid = types::Ssid::try_from("bar").unwrap();
let bss_description = random_fidl_bss_description!(Wpa2, ssid: next_network_ssid.clone());
let connect_request = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: next_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
credential: Credential::Password(b"password".to_vec()),
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal,
}),
},
reason: types::ConnectReason::ProactiveNetworkSwitch,
};
let (connect_sender, _connect_receiver) = oneshot::channel();
let connecting_options = ConnectingOptions {
connect_responder: Some(connect_sender),
connect_request: connect_request.clone(),
attempt_counter: 0,
};
let (disconnect_sender, mut disconnect_receiver) = oneshot::channel();
// Include both a "previous" and "next" network
let disconnecting_options = DisconnectingOptions {
disconnect_responder: Some(disconnect_sender),
previous_network: Some((
types::NetworkIdentifier {
ssid: previous_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
fidl_policy::DisconnectStatus::ConnectionStopped,
)),
next_network: Some(connecting_options),
reason: types::DisconnectReason::ProactiveNetworkSwitch,
};
let initial_state = disconnecting_state(test_values.common_options, disconnecting_options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
let sme_fut = test_values.sme_req_stream.into_future();
pin_mut!(sme_fut);
// Run the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Ensure a disconnect request is sent to the SME
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch }) => {
responder.send().expect("could not send sme response");
}
);
// Progress the state machine
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Check for a disconnect update and the disconnect responder
let client_state_update = ClientStateUpdate {
state: fidl_policy::WlanClientState::ConnectionsEnabled,
networks: vec![ClientNetworkState {
id: types::NetworkIdentifier {
ssid: previous_network_ssid.clone(),
security_type: types::SecurityType::Wpa2,
},
state: fidl_policy::ConnectionState::Disconnected,
status: Some(fidl_policy::DisconnectStatus::ConnectionStopped),
}],
};
assert_variant!(
test_values.update_receiver.try_next(),
Ok(Some(listener::Message::NotifyListeners(updates))) => {
assert_eq!(updates, client_state_update);
});
assert_variant!(exec.run_until_stalled(&mut disconnect_receiver), Poll::Ready(Ok(())));
// Ensure a connect request is sent to the SME
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
assert_eq!(req.ssid, next_network_ssid.to_vec());
assert_eq!(connect_request.target.credential, req.authentication.credentials.into());
assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
assert_eq!(req.bss_description, bss_description.clone());
assert_eq!(req.multiple_bss_candidates, true);
// Send connection response.
let (_stream, ctrl) = txn.expect("connect txn unused")
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl
.send_on_connect_result(&mut fake_successful_connect_result())
.expect("failed to send connection completion");
}
);
// Cobalt metrics logged
validate_cobalt_events!(
test_values.cobalt_events,
CONNECTION_ATTEMPT_METRIC_ID,
types::ConnectReason::ProactiveNetworkSwitch
);
}
#[fuchsia::test]
fn disconnecting_state_has_broken_sme() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
let test_values = test_setup();
let (sender, mut receiver) = oneshot::channel();
let disconnecting_options = DisconnectingOptions {
disconnect_responder: Some(sender),
previous_network: None,
next_network: None,
reason: types::DisconnectReason::NetworkConfigUpdated,
};
let initial_state = disconnecting_state(test_values.common_options, disconnecting_options);
let fut = run_state_machine(initial_state);
pin_mut!(fut);
// Break the SME by dropping the server end of the SME stream, so it causes an error
drop(test_values.sme_req_stream);
// Ensure the state machine exits
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
// Expect the responder to have an error
assert_variant!(exec.run_until_stalled(&mut receiver), Poll::Ready(Err(_)));
}
#[fuchsia::test]
fn serve_loop_handles_startup() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
let test_values = test_setup();
let sme_proxy = test_values.common_options.proxy;
let sme_event_stream = sme_proxy.take_event_stream();
let (_client_req_sender, client_req_stream) = mpsc::channel(1);
let sme_fut = test_values.sme_req_stream.into_future();
pin_mut!(sme_fut);
// Create a connect request so that the state machine does not immediately exit.
let connect_req = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: types::Ssid::try_from("no_password").unwrap(),
security_type: types::SecurityType::None,
},
credential: Credential::None,
scanned: None,
},
reason: types::ConnectReason::IdleInterfaceAutoconnect,
};
let (sender, _receiver) = oneshot::channel();
let fut = serve(
0,
false,
sme_proxy,
sme_event_stream,
client_req_stream,
test_values.common_options.update_sender,
test_values.common_options.saved_networks_manager,
Some((connect_req, sender)),
test_values.common_options.network_selector,
test_values.common_options.cobalt_api,
test_values.common_options.telemetry_sender,
test_values.common_options.stats_sender,
);
pin_mut!(fut);
// Run the state machine so it sends the initial SME disconnect request.
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::Startup }) => {
responder.send().expect("could not send sme response");
}
);
// Run the future again and ensure that it has not exited after receiving the response.
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
}
#[fuchsia::test]
fn serve_loop_handles_sme_disappearance() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
let test_values = test_setup();
let (_client_req_sender, client_req_stream) = mpsc::channel(1);
// Make our own SME proxy for this test
let (sme_proxy, sme_server) =
create_proxy::<fidl_sme::ClientSmeMarker>().expect("failed to create an sme channel");
let (sme_req_stream, sme_control_handle) = sme_server
.into_stream_and_control_handle()
.expect("could not create SME request stream");
let sme_fut = sme_req_stream.into_future();
pin_mut!(sme_fut);
let sme_event_stream = sme_proxy.take_event_stream();
// Create a connect request so that the state machine does not immediately exit.
let connect_req = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: types::Ssid::try_from("no_password").unwrap(),
security_type: types::SecurityType::None,
},
credential: Credential::None,
scanned: None,
},
reason: types::ConnectReason::FidlConnectRequest,
};
let (sender, _receiver) = oneshot::channel();
let fut = serve(
0,
false,
sme_proxy,
sme_event_stream,
client_req_stream,
test_values.common_options.update_sender,
test_values.common_options.saved_networks_manager,
Some((connect_req, sender)),
test_values.common_options.network_selector,
test_values.common_options.cobalt_api,
test_values.common_options.telemetry_sender,
test_values.common_options.stats_sender,
);
pin_mut!(fut);
// Run the state machine so it sends the initial SME disconnect request.
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::Startup }) => {
responder.send().expect("could not send sme response");
}
);
// Run the future again and ensure that it has not exited after receiving the response.
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
sme_control_handle.shutdown_with_epitaph(zx::Status::UNAVAILABLE);
// Ensure the state machine has no further actions and is exited
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
}
#[fuchsia::test]
fn serve_loop_handles_disconnect() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
let mut test_values = test_setup();
let sme_proxy = test_values.common_options.proxy;
let sme_event_stream = sme_proxy.take_event_stream();
let (client_req_sender, client_req_stream) = mpsc::channel(1);
let sme_fut = test_values.sme_req_stream.into_future();
pin_mut!(sme_fut);
// Create a connect request so that the state machine does not immediately exit.
let ssid = "no_password".as_bytes().to_vec();
let bss_description = random_fidl_bss_description!(Open);
let connect_req = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: types::Ssid::try_from(ssid.clone()).unwrap(),
security_type: types::SecurityType::None,
},
credential: Credential::None,
scanned: Some(types::ScannedCandidate {
bss_description: bss_description.clone(),
observation: types::ScanObservation::Passive,
has_multiple_bss_candidates: true,
security_type_detailed: types::SecurityTypeDetailed::Open,
}),
},
reason: types::ConnectReason::RegulatoryChangeReconnect,
};
let (sender, _receiver) = oneshot::channel();
let fut = serve(
0,
false,
sme_proxy,
sme_event_stream,
client_req_stream,
test_values.common_options.update_sender,
test_values.common_options.saved_networks_manager,
Some((connect_req, sender)),
test_values.common_options.network_selector,
test_values.common_options.cobalt_api,
test_values.common_options.telemetry_sender,
test_values.common_options.stats_sender,
);
pin_mut!(fut);
// Run the state machine so it sends the initial SME disconnect request.
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::Startup }) => {
responder.send().expect("could not send sme response");
}
);
// Run the future again and ensure that it has not exited after receiving the response.
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Absorb the connect request.
let connect_txn_handle = assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req: _, txn, control_handle: _ }) => {
// Send connection response.
let (_stream, ctrl) = txn.expect("connect txn unused")
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl
}
);
connect_txn_handle
.send_on_connect_result(&mut fake_successful_connect_result())
.expect("failed to send connection completion");
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Send a disconnect request
let mut client = Client::new(client_req_sender);
let (sender, mut receiver) = oneshot::channel();
client
.disconnect(PolicyDisconnectionMetricDimensionReason::NetworkConfigUpdated, sender)
.expect("failed to make request");
// Run the state machine so that it handles the disconnect message.
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
assert_variant!(
poll_sme_req(&mut exec, &mut sme_fut),
Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::NetworkConfigUpdated }) => {
responder.send().expect("could not send sme response");
}
);
// The state machine should exit following the disconnect request.
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
// Expect the responder to be acknowledged
assert_variant!(exec.run_until_stalled(&mut receiver), Poll::Ready(Ok(())));
// Cobalt metrics logged
validate_cobalt_events!(
test_values.cobalt_events,
CONNECTION_ATTEMPT_METRIC_ID,
types::ConnectReason::RegulatoryChangeReconnect
);
validate_cobalt_events!(
test_values.cobalt_events,
DISCONNECTION_METRIC_ID,
types::DisconnectReason::NetworkConfigUpdated
);
}
#[fuchsia::test]
fn serve_loop_handles_state_machine_error() {
let mut exec = fasync::TestExecutor::new().expect("failed to create an executor");
let test_values = test_setup();
let sme_proxy = test_values.common_options.proxy;
let sme_event_stream = sme_proxy.take_event_stream();
let (_client_req_sender, client_req_stream) = mpsc::channel(1);
// Create a connect request so that the state machine does not immediately exit.
let connect_req = types::ConnectRequest {
target: types::ConnectionCandidate {
network: types::NetworkIdentifier {
ssid: types::Ssid::try_from("no_password").unwrap(),
security_type: types::SecurityType::None,
},
credential: Credential::None,
scanned: None,
},
reason: types::ConnectReason::FidlConnectRequest,
};
let (sender, _receiver) = oneshot::channel();
let fut = serve(
0,
false,
sme_proxy,
sme_event_stream,
client_req_stream,
test_values.common_options.update_sender,
test_values.common_options.saved_networks_manager,
Some((connect_req, sender)),
test_values.common_options.network_selector,
test_values.common_options.cobalt_api,
test_values.common_options.telemetry_sender,
test_values.common_options.stats_sender,
);
pin_mut!(fut);
// Drop the server end of the SME stream, so it causes an error
drop(test_values.sme_req_stream);
// Ensure the state machine exits
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
}
fn fake_successful_connect_result() -> fidl_sme::ConnectResult {
fidl_sme::ConnectResult {
code: fidl_ieee80211::StatusCode::Success,
is_credential_rejected: false,
is_reconnect: false,
}
}
}