blob: 51f4b12a3f9563f1764ebfbf6fceee2e3ea61d1b [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
super::*,
crate::{
client::{ConnectFailure, ConnectResult},
Ssid,
},
fidl_fuchsia_wlan_mlme as fidl_mlme, fuchsia_zircon as zx,
std::collections::VecDeque,
thiserror::Error,
wlan_rsn::{
key::exchange::Key,
rsna::{SecAssocStatus, SecAssocUpdate, UpdateSink},
},
};
#[derive(Default)]
pub(crate) struct StatsCollector {
discovery_scan_stats: Option<PendingScanStats>,
/// Track successive connect attempts to the same SSID. This resets when attempt succeeds
/// or when attempting to connect to a different SSID from a previous attempt.
connect_attempts: Option<ConnectAttempts>,
/// Track the most recent disconnection. Intended to be sent out on connection success
/// so that client can compute time gap between last disconnect until reconnect.
/// This is cleared out as soon as a connection attempt succeeds.
previous_disconnect_info: Option<PreviousDisconnectInfo>,
}
impl StatsCollector {
pub fn report_join_scan_started(
&mut self,
req: fidl_mlme::ScanRequest,
is_connected: bool,
) -> Result<(), StatsError> {
let now = now();
let pending_scan_stats =
PendingScanStats { scan_start_at: now, req, scan_start_while_connected: is_connected };
self.connect_stats()?.pending_scan_stats.replace(pending_scan_stats);
Ok(())
}
/// Report the start of a new discovery scan so that StatsCollector will begin collecting
/// stats for it. If there's an existing pending scan stats, evict and return it.
pub fn report_discovery_scan_started(
&mut self,
req: fidl_mlme::ScanRequest,
is_connected: bool,
) -> Option<PendingScanStats> {
let now = now();
let pending_scan_stats =
PendingScanStats { scan_start_at: now, req, scan_start_while_connected: is_connected };
self.discovery_scan_stats.replace(pending_scan_stats)
}
pub fn report_discovery_scan_ended(
&mut self,
result: ScanResult,
bss_list: Option<&Vec<fidl_mlme::BssDescription>>,
) -> Result<ScanStats, StatsError> {
let now = now();
let pending_stats = self.discovery_scan_stats.take().ok_or(StatsError::NoPendingScan)?;
let scan_stats = ScanStats {
scan_type: pending_stats.req.scan_type,
scan_start_while_connected: pending_stats.scan_start_while_connected,
scan_start_at: pending_stats.scan_start_at,
scan_end_at: now,
result,
bss_count: bss_list.map(|bss_list| bss_list.len()).unwrap_or(0),
};
Ok(scan_stats)
}
pub fn report_join_scan_ended(
&mut self,
result: ScanResult,
bss_count: usize,
) -> Result<(), StatsError> {
let stats = ScanEndStats { scan_end_at: now(), result, bss_count };
self.connect_stats()?.scan_end_stats.replace(stats);
Ok(())
}
/// Report the start of a new connect attempt so that StatsCollector will begin collecting
/// stats for it. If there's an existing pending connect stats, evict and return it.
pub fn report_connect_started(&mut self, ssid: Ssid) -> Option<PendingConnectStats> {
match self.connect_attempts.as_mut() {
Some(connect_attempts) => connect_attempts.update_with_new_attempt(ssid),
None => {
self.connect_attempts = Some(ConnectAttempts::new(ssid));
None
}
}
}
pub fn report_candidate_network(
&mut self,
desc: fidl_mlme::BssDescription,
) -> Result<(), StatsError> {
self.connect_stats()?.candidate_network.replace(desc);
Ok(())
}
pub fn report_auth_started(&mut self) -> Result<(), StatsError> {
self.connect_stats()?.auth_start_at.replace(now());
Ok(())
}
pub fn report_assoc_started(&mut self) -> Result<(), StatsError> {
let pending_stats = self.connect_stats()?;
let now = now();
pending_stats.auth_end_at.replace(now);
pending_stats.assoc_start_at.replace(now);
Ok(())
}
pub fn report_assoc_success(&mut self) -> Result<(), StatsError> {
self.connect_stats()?.assoc_end_at.replace(now());
Ok(())
}
pub fn report_rsna_started(&mut self) -> Result<(), StatsError> {
self.connect_stats()?.rsna_start_at.replace(now());
Ok(())
}
pub fn report_rsna_established(&mut self) -> Result<(), StatsError> {
self.connect_stats()?.rsna_end_at.replace(now());
Ok(())
}
/// Report updates derived from the supplicant. Used to record progress of establish RSNA step.
pub fn report_supplicant_updates(
&mut self,
update_sink: &UpdateSink,
) -> Result<(), StatsError> {
let supplicant_progress =
self.connect_stats()?.supplicant_progress.get_or_insert(SupplicantProgress::default());
for update in update_sink {
match update {
SecAssocUpdate::Status(status) => match status {
SecAssocStatus::PmkSaEstablished => {
supplicant_progress.pmksa_established = true
}
SecAssocStatus::EssSaEstablished => {
supplicant_progress.esssa_established = true
}
_ => (),
},
SecAssocUpdate::Key(key) => match key {
Key::Ptk(..) => supplicant_progress.ptksa_established = true,
Key::Gtk(..) => supplicant_progress.gtksa_established = true,
_ => (),
},
_ => (),
}
}
Ok(())
}
pub fn report_supplicant_error(&mut self, error: anyhow::Error) -> Result<(), StatsError> {
self.connect_stats()?.supplicant_error.replace(error);
Ok(())
}
pub fn report_key_exchange_timeout(&mut self) -> Result<(), StatsError> {
self.connect_stats()?.num_rsna_key_frame_exchange_timeout += 1;
Ok(())
}
pub fn report_connect_finished(
&mut self,
result: ConnectResult,
) -> Result<ConnectStats, StatsError> {
let mut connect_attempts =
self.connect_attempts.take().ok_or(StatsError::NoPendingConnect)?;
let stats = self.finalize_connect_stats(&mut connect_attempts, result.clone())?;
if result != ConnectResult::Success {
// Successive connect attempts are still tracked unless connection is successful
self.connect_attempts.replace(connect_attempts);
}
Ok(stats)
}
fn connect_stats(&mut self) -> Result<&mut PendingConnectStats, StatsError> {
self.connect_attempts
.as_mut()
.and_then(|a| a.stats.as_mut())
.ok_or(StatsError::NoPendingConnect)
}
fn finalize_connect_stats(
&mut self,
connect_attempts: &mut ConnectAttempts,
result: ConnectResult,
) -> Result<ConnectStats, StatsError> {
let now = now();
let pending_stats = connect_attempts.handle_result(&result, now)?;
let previous_disconnect_info = match &result {
ConnectResult::Success => self.previous_disconnect_info.take(),
_ => None,
};
Ok(ConnectStats {
connect_start_at: pending_stats.connect_start_at,
connect_end_at: now,
scan_start_stats: pending_stats.pending_scan_stats.map(|stats| ScanStartStats {
scan_start_at: stats.scan_start_at,
scan_type: stats.req.scan_type,
scan_start_while_connected: stats.scan_start_while_connected,
}),
scan_end_stats: pending_stats.scan_end_stats,
auth_start_at: pending_stats.auth_start_at,
auth_end_at: pending_stats.auth_end_at,
assoc_start_at: pending_stats.assoc_start_at,
assoc_end_at: pending_stats.assoc_end_at,
rsna_start_at: pending_stats.rsna_start_at,
rsna_end_at: pending_stats.rsna_end_at,
result,
candidate_network: pending_stats.candidate_network,
supplicant_error: pending_stats.supplicant_error,
supplicant_progress: pending_stats.supplicant_progress,
num_rsna_key_frame_exchange_timeout: pending_stats.num_rsna_key_frame_exchange_timeout,
attempts: connect_attempts.attempts,
last_ten_failures: connect_attempts.last_ten_failures.iter().cloned().collect(),
previous_disconnect_info,
})
}
pub fn report_disconnect(&mut self, ssid: Ssid, source: DisconnectSource) {
self.previous_disconnect_info.replace(PreviousDisconnectInfo {
ssid,
disconnect_source: source,
disconnect_at: now(),
});
}
}
fn now() -> zx::Time {
zx::Time::get_monotonic()
}
struct ConnectAttempts {
ssid: Ssid,
attempts: u32,
last_ten_failures: VecDeque<ConnectFailure>,
stats: Option<PendingConnectStats>,
}
impl ConnectAttempts {
const MAX_FAILURES_TRACKED: usize = 10;
pub fn new(ssid: Ssid) -> Self {
Self {
ssid,
attempts: 1,
last_ten_failures: VecDeque::with_capacity(Self::MAX_FAILURES_TRACKED),
stats: Some(PendingConnectStats::new(now())),
}
}
/// Increment attempt counter if same SSID. Otherwise, reset and start tracking connect
/// attempts for the new SSID.
///
/// Additionally, evict and return any existing PendingConnectStats.
pub fn update_with_new_attempt(&mut self, ssid: Ssid) -> Option<PendingConnectStats> {
if self.ssid == ssid {
self.attempts += 1;
} else {
self.ssid = ssid;
self.attempts = 1;
self.last_ten_failures.clear();
}
self.stats.replace(PendingConnectStats::new(now()))
}
pub fn handle_result(
&mut self,
result: &ConnectResult,
now: zx::Time,
) -> Result<PendingConnectStats, StatsError> {
let mut pending_stats = self.stats.take().ok_or(StatsError::NoPendingConnect)?;
match result {
ConnectResult::Failed(failure) => match failure {
ConnectFailure::ScanFailure(code) => {
pending_stats.scan_end_stats.replace(ScanEndStats {
scan_end_at: now,
result: ScanResult::Failed(*code),
bss_count: 0,
});
}
ConnectFailure::AuthenticationFailure(..) => {
pending_stats.auth_end_at.replace(now);
}
ConnectFailure::AssociationFailure(..) => {
pending_stats.assoc_end_at.replace(now);
}
ConnectFailure::EstablishRsnaFailure(..) => {
pending_stats.rsna_end_at.replace(now);
}
_ => (),
},
_ => (),
}
if let ConnectResult::Failed(failure) = result {
if self.last_ten_failures.len() >= Self::MAX_FAILURES_TRACKED {
self.last_ten_failures.pop_front();
}
self.last_ten_failures.push_back(failure.clone());
}
Ok(pending_stats)
}
}
#[derive(Debug, Error)]
pub(crate) enum StatsError {
#[error("no current pending connect")]
NoPendingConnect,
#[error("no current pending scan")]
NoPendingScan,
}
pub(crate) struct PendingScanStats {
scan_start_at: zx::Time,
req: fidl_mlme::ScanRequest,
scan_start_while_connected: bool,
}
pub(crate) struct PendingConnectStats {
connect_start_at: zx::Time,
pending_scan_stats: Option<PendingScanStats>,
scan_end_stats: Option<ScanEndStats>,
auth_start_at: Option<zx::Time>,
auth_end_at: Option<zx::Time>,
assoc_start_at: Option<zx::Time>,
assoc_end_at: Option<zx::Time>,
rsna_start_at: Option<zx::Time>,
rsna_end_at: Option<zx::Time>,
candidate_network: Option<fidl_mlme::BssDescription>,
supplicant_error: Option<anyhow::Error>,
supplicant_progress: Option<SupplicantProgress>,
num_rsna_key_frame_exchange_timeout: u32,
}
impl PendingConnectStats {
fn new(connect_start_at: zx::Time) -> Self {
Self {
connect_start_at,
pending_scan_stats: None,
scan_end_stats: None,
auth_start_at: None,
auth_end_at: None,
assoc_start_at: None,
assoc_end_at: None,
rsna_start_at: None,
rsna_end_at: None,
candidate_network: None,
supplicant_error: None,
supplicant_progress: None,
num_rsna_key_frame_exchange_timeout: 0,
}
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::client::{
test_utils::fake_scan_request, EstablishRsnaFailure, EstablishRsnaFailureReason,
SelectNetworkFailure,
},
anyhow::format_err,
wlan_common::{assert_variant, fake_bss},
wlan_rsn::auth,
};
const SSID: &[u8; 3] = b"foo";
#[test]
fn test_discovery_scan_stats_lifecycle() {
let mut stats_collector = StatsCollector::default();
let req = fake_scan_request();
let is_connected = true;
assert!(stats_collector.report_discovery_scan_started(req, is_connected).is_none());
let bss_desc = fake_bss!(Open, ssid: SSID.to_vec(), rates: vec![12]);
let stats =
stats_collector.report_discovery_scan_ended(ScanResult::Success, Some(&vec![bss_desc]));
assert_variant!(stats, Ok(scan_stats) => {
assert!(scan_stats.scan_time().into_nanos() > 0);
assert_eq!(scan_stats.scan_type, fidl_mlme::ScanTypes::Active);
assert_eq!(scan_stats.scan_start_while_connected, is_connected);
assert_eq!(scan_stats.result, ScanResult::Success);
assert_eq!(scan_stats.bss_count, 1);
})
}
#[test]
fn test_connect_stats_lifecycle() {
let mut stats_collector = StatsCollector::default();
let stats = simulate_connect_lifecycle(&mut stats_collector);
assert_variant!(stats, Ok(stats) => {
assert!(stats.connect_time().into_nanos() > 0);
assert_variant!(stats.join_scan_stats(), Some(scan_stats) => {
assert!(scan_stats.scan_time().into_nanos() > 0);
assert_eq!(scan_stats.scan_type, fidl_mlme::ScanTypes::Active);
assert_eq!(scan_stats.scan_start_while_connected, false);
assert_eq!(scan_stats.result, ScanResult::Success);
assert_eq!(scan_stats.bss_count, 1);
});
assert!(stats.auth_time().is_some());
assert!(stats.assoc_time().is_some());
assert!(stats.rsna_time().is_some());
assert_eq!(stats.result, ConnectResult::Success);
let bss_desc = fake_bss!(Wpa2, ssid: SSID.to_vec());
assert_eq!(stats.candidate_network, Some(bss_desc));
});
}
#[test]
fn test_connect_stats_finalized_midway() {
let mut stats_collector = StatsCollector::default();
assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none());
let scan_req = fake_scan_request();
assert!(stats_collector.report_join_scan_started(scan_req, false).is_ok());
assert!(stats_collector.report_join_scan_ended(ScanResult::Success, 1).is_ok());
let bss_desc = fake_bss!(Wpa2, ssid: SSID.to_vec());
assert!(stats_collector.report_candidate_network(bss_desc).is_ok());
assert!(stats_collector.report_auth_started().is_ok());
let result = ConnectResult::Failed(ConnectFailure::AuthenticationFailure(
fidl_mlme::AuthenticateResultCodes::Refused,
));
let stats = stats_collector.report_connect_finished(result.clone());
assert_variant!(stats, Ok(stats) => {
assert!(stats.connect_time().into_nanos() > 0);
assert!(stats.join_scan_stats().is_some());
assert!(stats.auth_time().is_some());
assert!(stats.assoc_time().is_none());
assert!(stats.rsna_time().is_none());
assert_eq!(stats.result, result);
assert!(stats.candidate_network.is_some());
});
}
#[test]
fn test_connect_stats_establish_rsna_failure() {
let mut stats_collector = StatsCollector::default();
assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none());
// Connecting should complete other steps first before starting establish RSNA step,
// but for testing starting with RSNA step right away is sufficient.
assert!(stats_collector.report_rsna_started().is_ok());
let update_sink = vec![SecAssocUpdate::Status(SecAssocStatus::PmkSaEstablished)];
assert!(stats_collector.report_supplicant_updates(&update_sink).is_ok());
assert!(stats_collector.report_key_exchange_timeout().is_ok());
assert!(stats_collector.report_key_exchange_timeout().is_ok());
assert!(stats_collector.report_supplicant_error(format_err!("blah")).is_ok());
let stats = stats_collector.report_connect_finished(
EstablishRsnaFailure {
auth_method: Some(auth::MethodName::Psk),
reason: EstablishRsnaFailureReason::OverallTimeout,
}
.into(),
);
assert_variant!(stats, Ok(stats) => {
assert!(stats.supplicant_error.is_some());
assert_variant!(stats.supplicant_progress, Some(SupplicantProgress {
pmksa_established: true,
ptksa_established: false,
gtksa_established: false,
esssa_established: false,
}));
assert_eq!(stats.num_rsna_key_frame_exchange_timeout, 2);
});
}
#[test]
fn test_consecutive_connect_attempts_stats() {
let mut stats_collector = StatsCollector::default();
assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none());
let failure1: ConnectFailure = SelectNetworkFailure::NoScanResultWithSsid.into();
let stats = stats_collector.report_connect_finished(failure1.clone().into());
assert_variant!(stats, Ok(stats) => {
assert_eq!(stats.attempts, 1);
assert_eq!(stats.last_ten_failures, &[failure1.clone()])
});
assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none());
let failure2: ConnectFailure = EstablishRsnaFailure {
auth_method: Some(auth::MethodName::Psk),
reason: EstablishRsnaFailureReason::OverallTimeout,
}
.into();
let stats = stats_collector.report_connect_finished(failure2.clone().into());
assert_variant!(stats, Ok(stats) => {
assert_eq!(stats.attempts, 2);
assert_eq!(stats.last_ten_failures, &[failure1.clone(), failure2.clone()]);
});
assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none());
let stats = stats_collector.report_connect_finished(ConnectResult::Success);
assert_variant!(stats, Ok(stats) => {
assert_eq!(stats.attempts, 3);
assert_eq!(stats.last_ten_failures, &[failure1.clone(), failure2.clone()]);
});
// After a successful connection, new connect attempts tracking is reset
assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none());
let stats = stats_collector.report_connect_finished(ConnectResult::Success);
assert_variant!(stats, Ok(stats) => {
assert_eq!(stats.attempts, 1);
assert_eq!(stats.last_ten_failures, &[])
});
}
#[test]
fn test_consecutive_connect_attempts_different_ssid_resets_stats() {
let mut stats_collector = StatsCollector::default();
assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none());
let failure1: ConnectFailure = SelectNetworkFailure::NoScanResultWithSsid.into();
let _stats = stats_collector.report_connect_finished(failure1.clone().into());
assert!(stats_collector.report_connect_started(b"bar".to_vec()).is_none());
let failure2: ConnectFailure = EstablishRsnaFailure {
auth_method: Some(auth::MethodName::Psk),
reason: EstablishRsnaFailureReason::OverallTimeout,
}
.into();
let stats = stats_collector.report_connect_finished(failure2.clone().into());
assert_variant!(stats, Ok(stats) => {
assert_eq!(stats.attempts, 1);
assert_eq!(stats.last_ten_failures, &[failure2.clone()]);
});
}
#[test]
fn test_consecutive_connect_attempts_only_ten_failures_are_tracked() {
let mut stats_collector = StatsCollector::default();
for i in 1..=20 {
assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none());
let stats = stats_collector
.report_connect_finished(SelectNetworkFailure::NoScanResultWithSsid.into());
assert_variant!(stats, Ok(stats) => {
assert_eq!(stats.attempts, i);
assert_eq!(stats.last_ten_failures.len(), std::cmp::min(i as usize, 10));
});
}
}
#[test]
fn test_disconnect_then_reconnect() {
let mut stats_collector = StatsCollector::default();
let stats = simulate_connect_lifecycle(&mut stats_collector);
assert_variant!(stats, Ok(stats) => stats.previous_disconnect_info.is_none());
stats_collector.report_disconnect(b"foo".to_vec(), DisconnectSource::User);
let stats = simulate_connect_lifecycle(&mut stats_collector);
assert_variant!(stats, Ok(stats) => {
assert_variant!(stats.previous_disconnect_info, Some(info) => {
assert_eq!(info.disconnect_source, DisconnectSource::User);
})
});
}
#[test]
fn test_disconnect_then_connect_fails_before_succeeding() {
let mut stats_collector = StatsCollector::default();
// Connects then disconnect
let stats = simulate_connect_lifecycle(&mut stats_collector);
assert_variant!(stats, Ok(stats) => stats.previous_disconnect_info.is_none());
stats_collector.report_disconnect(b"foo".to_vec(), DisconnectSource::User);
// Attempt to connect but fails
assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none());
let scan_req = fake_scan_request();
assert!(stats_collector.report_join_scan_started(scan_req, false).is_ok());
let failure = ConnectFailure::ScanFailure(fidl_mlme::ScanResultCodes::InternalError).into();
let stats = stats_collector.report_connect_finished(failure);
// Previous disconnect info should not be reported on fail attempt
assert_variant!(stats, Ok(stats) => stats.previous_disconnect_info.is_none());
// Connect now succeeds, hence previous disconnect info is reported
let stats = simulate_connect_lifecycle(&mut stats_collector);
assert_variant!(stats, Ok(stats) => {
assert_variant!(stats.previous_disconnect_info, Some(info) => {
assert_eq!(info.disconnect_source, DisconnectSource::User);
})
});
}
#[test]
fn test_no_pending_discovery_scan_stats() {
let mut stats_collector = StatsCollector::default();
let bss_desc = fake_bss!(Wpa2, ssid: SSID.to_vec());
let stats =
stats_collector.report_discovery_scan_ended(ScanResult::Success, Some(&vec![bss_desc]));
assert_variant!(stats, Err(StatsError::NoPendingScan));
}
#[test]
fn test_no_pending_connect_stats() {
assert_variant!(
StatsCollector::default().report_join_scan_ended(ScanResult::Success, 1),
Err(StatsError::NoPendingConnect)
);
let bss_desc = fake_bss!(Wpa2, ssid: SSID.to_vec());
assert_variant!(
StatsCollector::default().report_candidate_network(bss_desc),
Err(StatsError::NoPendingConnect)
);
assert_variant!(
StatsCollector::default().report_auth_started(),
Err(StatsError::NoPendingConnect)
);
assert_variant!(
StatsCollector::default().report_assoc_started(),
Err(StatsError::NoPendingConnect)
);
assert_variant!(
StatsCollector::default().report_assoc_success(),
Err(StatsError::NoPendingConnect)
);
assert_variant!(
StatsCollector::default().report_rsna_started(),
Err(StatsError::NoPendingConnect)
);
assert_variant!(
StatsCollector::default().report_rsna_established(),
Err(StatsError::NoPendingConnect)
);
assert_variant!(
StatsCollector::default().report_connect_finished(ConnectResult::Success),
Err(StatsError::NoPendingConnect)
);
}
fn simulate_connect_lifecycle(
stats_collector: &mut StatsCollector,
) -> Result<ConnectStats, StatsError> {
assert!(stats_collector.report_connect_started(SSID.to_vec()).is_none());
let scan_req = fake_scan_request();
assert!(stats_collector.report_join_scan_started(scan_req, false).is_ok());
assert!(stats_collector.report_join_scan_ended(ScanResult::Success, 1).is_ok());
let bss_desc = fake_bss!(Wpa2, ssid: SSID.to_vec());
assert!(stats_collector.report_candidate_network(bss_desc).is_ok());
assert!(stats_collector.report_auth_started().is_ok());
assert!(stats_collector.report_assoc_started().is_ok());
assert!(stats_collector.report_assoc_success().is_ok());
assert!(stats_collector.report_rsna_started().is_ok());
assert!(stats_collector.report_rsna_established().is_ok());
stats_collector.report_connect_finished(ConnectResult::Success)
}
}