blob: ee5e3f286d4732d5cef0ec2abe1088812ea7933d [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 {
fidl_fuchsia_wlan_policy as fidl_policy, fidl_fuchsia_wlan_sme as fidl_sme,
serde_derive::{Deserialize, Serialize},
std::{collections::VecDeque, time::SystemTime},
wlan_common::mac::Bssid,
};
/// The maximum number of denied connection reasons we will store for one network at a time.
/// For now this number is chosen arbitrarily.
const NUM_DENY_REASONS: usize = 10;
/// constants for the constraints on valid credential values
const MIN_PASSWORD_LEN: usize = 8;
const MAX_PASSWORD_LEN: usize = 63;
const PSK_LEN: usize = 64;
type SaveError = fidl_policy::NetworkConfigChangeError;
/// History of connects, disconnects, and connection strength to estimate whether we can establish
/// and maintain connection with a network and if it is weakening. Used in choosing best network.
#[derive(Clone, Debug, PartialEq)]
pub struct PerformanceStats {
/// List of recent connection denials, used to determine whether we should try connecting
/// to a network again. Capacity of list is at least NUM_DENY_REASONS.
deny_list: NetworkDenialList,
}
impl PerformanceStats {
pub fn new() -> Self {
Self { deny_list: NetworkDenialList::new() }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct NetworkDenial {
/// Remember which AP of a network was denied
bssid: Bssid,
/// Determine whether this network denial is still relevant
time: SystemTime,
/// The reason that connection was denied
reason: fidl_sme::ConnectResultCode,
}
/// Ring buffer that holds network denials. It starts empty and replaces oldest when full.
#[derive(Clone, Debug, PartialEq)]
pub struct NetworkDenialList(VecDeque<NetworkDenial>);
impl NetworkDenialList {
/// The max stored number of deny reasons is at least NUM_DENY_REASONS, decided by VecDeque
pub fn new() -> Self {
Self(VecDeque::with_capacity(NUM_DENY_REASONS))
}
/// This function will be used in future work when Network Denial reasons are recorded.
/// Record network denial information in the network config, dropping the oldest information
/// if the list of denial reasons is already full before adding.
#[allow(dead_code)]
pub fn add(&mut self, bssid: Bssid, reason: fidl_sme::ConnectResultCode) {
if self.0.len() == self.0.capacity() {
self.0.pop_front();
}
self.0.push_back(NetworkDenial { bssid, time: SystemTime::now(), reason });
}
/// This function will be used when Network Denial reasons are used to select a network.
/// Returns a list of the denials that happened at or after given system time.
#[allow(dead_code)]
pub fn get_recent(&self, earliest_time: SystemTime) -> Vec<NetworkDenial> {
self.0.iter().skip_while(|denial| denial.time < earliest_time).cloned().collect()
}
}
/// Saved data for networks, to remember how to connect to a network and determine if we should.
#[derive(Clone, Debug, PartialEq)]
pub struct NetworkConfig {
/// (persist) SSID and security type to identify a network.
pub ssid: Vec<u8>,
pub security_type: SecurityType,
/// (persist) Credential to connect to a protected network or None if the network is open.
pub credential: Credential,
/// (persist) Remember whether our network indentifier and credential work.
pub has_ever_connected: bool,
/// Used to differentiate hidden networks when doing active scans.
pub seen_in_passive_scan_results: bool,
/// Used to estimate quality to determine whether we want to choose this network.
pub perf_stats: PerformanceStats,
}
impl NetworkConfig {
pub fn new(
id: NetworkIdentifier,
credential: Credential,
has_ever_connected: bool,
seen_in_passive_scan_results: bool,
) -> Result<Self, SaveError> {
check_config_errors(&id.ssid, &id.security_type, &credential)?;
Ok(Self {
ssid: id.ssid,
security_type: id.security_type,
credential,
has_ever_connected,
seen_in_passive_scan_results,
perf_stats: PerformanceStats::new(),
})
}
}
/// The credential of a network connection. It mirrors the fidl_fuchsia_wlan_policy Credential
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum Credential {
None,
Password(Vec<u8>),
Psk(Vec<u8>),
}
/// Returns:
/// - an Open-Credential instance iff `bytes` is empty,
/// - a PSK-Credential instance iff `bytes` holds exactly 64 bytes,
/// - a Password-Credential in all other cases.
/// In the PSK case, the provided bytes must represent the PSK in hex format.
/// Note: This function is of temporary nature until connection results communicate
/// type of credential
impl Credential {
pub fn from_bytes(bytes: impl AsRef<[u8]> + Into<Vec<u8>>) -> Self {
const PSK_HEX_STRING_LENGTH: usize = 64;
match bytes.as_ref().len() {
0 => Credential::None,
PSK_HEX_STRING_LENGTH => Credential::Psk(bytes.into()),
_ => Credential::Password(bytes.into()),
}
}
/// Choose a security type that fits the credential while we don't actually know the security type
/// of the saved networks. This should only be used if we don't have a specified security type.
pub fn derived_security_type(&self) -> SecurityType {
match self {
Credential::None => SecurityType::None,
_ => SecurityType::Wpa2,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum SecurityType {
None,
Wep,
Wpa,
Wpa2,
Wpa3,
}
impl From<fidl_policy::SecurityType> for SecurityType {
fn from(security: fidl_policy::SecurityType) -> Self {
match security {
fidl_policy::SecurityType::None => SecurityType::None,
fidl_policy::SecurityType::Wep => SecurityType::Wep,
fidl_policy::SecurityType::Wpa => SecurityType::Wpa,
fidl_policy::SecurityType::Wpa2 => SecurityType::Wpa2,
fidl_policy::SecurityType::Wpa3 => SecurityType::Wpa3,
}
}
}
impl From<SecurityType> for fidl_policy::SecurityType {
fn from(security_type: SecurityType) -> Self {
match security_type {
SecurityType::None => fidl_policy::SecurityType::None,
SecurityType::Wep => fidl_policy::SecurityType::Wep,
SecurityType::Wpa => fidl_policy::SecurityType::Wpa,
SecurityType::Wpa2 => fidl_policy::SecurityType::Wpa2,
SecurityType::Wpa3 => fidl_policy::SecurityType::Wpa3,
}
}
}
/// The network identifier is the SSID and security policy of the network, and it is used to
/// distinguish networks. It mirrors the NetworkIdentifier in fidl_fuchsia_wlan_policy.
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct NetworkIdentifier {
pub ssid: Vec<u8>,
pub security_type: SecurityType,
}
impl NetworkIdentifier {
pub fn new(ssid: impl Into<Vec<u8>>, security_type: SecurityType) -> Self {
NetworkIdentifier { ssid: ssid.into(), security_type }
}
}
impl From<fidl_policy::NetworkIdentifier> for NetworkIdentifier {
fn from(id: fidl_policy::NetworkIdentifier) -> Self {
Self::new(id.ssid, id.type_.into())
}
}
impl From<NetworkIdentifier> for fidl_policy::NetworkIdentifier {
fn from(id: NetworkIdentifier) -> Self {
fidl_policy::NetworkIdentifier { ssid: id.ssid, type_: id.security_type.into() }
}
}
/// Returns an error if the input network values are not valid or none if the values are valid.
/// For example it is an error if the network is Open (no password) but a password is supplied.
/// TODO(nmccracken) - Specific errors need to be added to the API and returned here
fn check_config_errors(
ssid: impl AsRef<[u8]>,
security_type: &SecurityType,
credential: &Credential,
) -> Result<(), SaveError> {
// Verify SSID has at most 32 characters
if ssid.as_ref().len() > 32 {
return Err(SaveError::GeneralError);
}
// Verify that credentials match security type
match security_type {
SecurityType::None => {
if let Credential::Psk(_) | Credential::Password(_) = credential {
return Err(SaveError::GeneralError);
}
}
SecurityType::Wpa | SecurityType::Wpa2 | SecurityType::Wpa3 => match credential {
Credential::Password(pwd) => {
if pwd.len() < MIN_PASSWORD_LEN || pwd.len() > MAX_PASSWORD_LEN {
return Err(SaveError::GeneralError);
}
}
Credential::Psk(psk) => {
if psk.len() != PSK_LEN {
return Err(SaveError::GeneralError);
}
}
_ => {
return Err(SaveError::GeneralError);
}
},
_ => {}
}
Ok(())
}
#[cfg(test)]
mod tests {
use {
super::*,
std::{thread, time::Duration},
wlan_common::assert_variant,
};
#[test]
fn new_network_config_none_credential() {
let credential = Credential::None;
let network_config = NetworkConfig::new(
NetworkIdentifier::new("foo", SecurityType::None),
credential,
false,
false,
)
.expect("Error creating network config for foo");
assert_eq!(network_config.ssid, b"foo".to_vec());
assert_eq!(network_config.security_type, SecurityType::None);
assert_eq!(network_config.credential, Credential::None);
assert_eq!(network_config.has_ever_connected, false);
assert!(network_config.perf_stats.deny_list.0.is_empty());
}
#[test]
fn new_network_config_password_credential() {
let credential = Credential::Password(b"foo-password".to_vec());
let network_config = NetworkConfig::new(
NetworkIdentifier::new("foo", SecurityType::Wpa2),
credential,
false,
false,
)
.expect("Error creating network config for foo");
assert_eq!(network_config.ssid, b"foo".to_vec());
assert_eq!(network_config.security_type, SecurityType::Wpa2);
assert_eq!(network_config.credential, Credential::Password(b"foo-password".to_vec()));
assert_eq!(network_config.has_ever_connected, false);
assert!(network_config.perf_stats.deny_list.0.is_empty());
}
#[test]
fn new_network_config_psk_credential() {
let credential = Credential::Psk([1; 64].to_vec());
let network_config = NetworkConfig::new(
NetworkIdentifier::new("foo", SecurityType::Wpa2),
credential,
false,
false,
)
.expect("Error creating network config for foo");
assert_eq!(network_config.ssid, b"foo".to_vec());
assert_eq!(network_config.security_type, SecurityType::Wpa2);
assert_eq!(network_config.credential, Credential::Psk([1; 64].to_vec()));
assert_eq!(network_config.has_ever_connected, false);
assert!(network_config.perf_stats.deny_list.0.is_empty());
}
#[test]
fn new_network_config_invalid_ssid() {
let credential = Credential::None;
let network_config = NetworkConfig::new(
NetworkIdentifier::new([1; 33].to_vec(), SecurityType::Wpa2),
credential,
false,
false,
);
assert_variant!(network_config, Result::Err(err) =>
{assert_eq!(err, SaveError::GeneralError);}
);
}
#[test]
fn new_network_config_invalid_password() {
let credential = Credential::Password([1; 64].to_vec());
let network_config = NetworkConfig::new(
NetworkIdentifier::new("foo", SecurityType::Wpa),
credential,
false,
false,
);
assert_variant!(network_config, Result::Err(err) =>
{assert_eq!(err, SaveError::GeneralError);}
);
}
#[test]
fn new_network_config_invalid_psk() {
let credential = Credential::Psk(b"bar".to_vec());
let network_config = NetworkConfig::new(
NetworkIdentifier::new("foo", SecurityType::Wpa2),
credential,
false,
false,
);
assert_variant!(network_config, Result::Err(err) =>
{assert_eq!(err, SaveError::GeneralError);}
);
}
#[test]
fn check_confing_errors_invalid_password() {
// password too short
let short_password = Credential::Password(b"1234567".to_vec());
assert_eq!(
Err(SaveError::GeneralError),
check_config_errors(&b"valid_ssid".to_vec(), &SecurityType::Wpa2, &short_password)
);
// password too long
let long_password = Credential::Password([5, 65].to_vec());
assert_eq!(
Err(SaveError::GeneralError),
check_config_errors(&b"valid_ssid".to_vec(), &SecurityType::Wpa2, &long_password)
);
}
#[test]
fn check_config_errors_invalid_psk() {
// PSK length not 64 characters
let short_psk = Credential::Psk([6, 63].to_vec());
assert_eq!(
Err(SaveError::GeneralError),
check_config_errors(&b"valid_ssid".to_vec(), &SecurityType::Wpa2, &short_psk)
);
let long_psk = Credential::Psk([7, 65].to_vec());
assert_eq!(
Err(SaveError::GeneralError),
check_config_errors(&b"valid_ssid".to_vec(), &SecurityType::Wpa2, &long_psk)
);
}
#[test]
fn check_config_errors_invalid_security_credential() {
// Use a password with open network.
let password = Credential::Password(b"password".to_vec());
assert_eq!(
Err(SaveError::GeneralError),
check_config_errors(&b"valid_ssid".to_vec(), &SecurityType::None, &password)
);
let psk = Credential::Psk([1, 64].to_vec());
assert_eq!(
Err(SaveError::GeneralError),
check_config_errors(&b"valid_ssid".to_vec(), &SecurityType::None, &psk)
);
// Use no password with a protected network.
let password = Credential::None;
assert_eq!(
Err(SaveError::GeneralError),
check_config_errors(&b"valid_ssid".to_vec(), &SecurityType::Wpa, &password)
);
assert_eq!(
Err(SaveError::GeneralError),
check_config_errors(&b"valid_ssid".to_vec(), &SecurityType::Wpa2, &password)
);
assert_eq!(
Err(SaveError::GeneralError),
check_config_errors(&b"valid_ssid".to_vec(), &SecurityType::Wpa3, &password)
);
}
#[test]
fn checkout_config_errors_invalid_ssid() {
// The longest valid SSID length is 32, so 33 characters is too long.
let long_ssid = [64; 33].to_vec();
assert_eq!(
Err(SaveError::GeneralError),
check_config_errors(&long_ssid, &SecurityType::None, &Credential::None)
);
}
#[test]
fn deny_list_add_and_get() {
let mut deny_list = NetworkDenialList::new();
// Get time before adding so we can get back everything we added.
let curr_time = SystemTime::now();
assert!(deny_list.get_recent(curr_time).is_empty());
deny_list.add(Bssid([1, 2, 3, 4, 5, 6]), fidl_sme::ConnectResultCode::Failed);
let result_list = deny_list.get_recent(curr_time);
assert_eq!(1, result_list.len());
assert_eq!([1, 2, 3, 4, 5, 6], result_list[0].bssid.0);
assert_eq!(fidl_sme::ConnectResultCode::Failed, result_list[0].reason);
// Should not get any results if we request for more recent denials more recent than added.
assert!(deny_list.get_recent(SystemTime::now() + Duration::new(0, 1)).is_empty());
}
#[test]
fn deny_list_add_when_full() {
let mut deny_list = NetworkDenialList::new();
let curr_time = SystemTime::now();
assert!(deny_list.get_recent(curr_time).is_empty());
let deny_list_capacity = deny_list.0.capacity();
assert!(deny_list_capacity >= NUM_DENY_REASONS);
for _i in 0..deny_list_capacity + 2 {
deny_list.add(Bssid([1, 2, 3, 4, 5, 6]), fidl_sme::ConnectResultCode::Failed);
}
// Since we do not know time of each entry in the list, check the other values and length
assert_eq!(deny_list_capacity, deny_list.get_recent(curr_time).len());
for denial in deny_list.get_recent(curr_time) {
assert_eq!([1, 2, 3, 4, 5, 6], denial.bssid.0);
assert_eq!(fidl_sme::ConnectResultCode::Failed, denial.reason);
}
}
#[test]
fn get_part_of_deny_list() {
let mut deny_list = NetworkDenialList::new();
deny_list.add(Bssid([6, 5, 4, 3, 2, 1]), fidl_sme::ConnectResultCode::Failed);
// Choose half capacity to get so that we know the previous one is still in list
let half_capacity = deny_list.0.capacity() / 2;
// Ensure we get a time after the deny entry we don't want
thread::sleep(Duration::new(0, 1));
// curr_time is before the part of the list we want and after the one we don't want
let curr_time = SystemTime::now();
for _ in 0..half_capacity {
deny_list.add(Bssid([1, 2, 3, 4, 5, 6]), fidl_sme::ConnectResultCode::Failed);
}
// Since we do not know time of each entry in the list, check the other values and length
assert_eq!(half_capacity, deny_list.get_recent(curr_time).len());
for denial in deny_list.get_recent(curr_time) {
assert_eq!([1, 2, 3, 4, 5, 6], denial.bssid.0);
assert_eq!(fidl_sme::ConnectResultCode::Failed, denial.reason);
}
// Add one more and check list again
deny_list.add(Bssid([1, 2, 3, 4, 5, 6]), fidl_sme::ConnectResultCode::Failed);
assert_eq!(half_capacity + 1, deny_list.get_recent(curr_time).len());
for denial in deny_list.get_recent(curr_time) {
assert_eq!([1, 2, 3, 4, 5, 6], denial.bssid.0);
assert_eq!(fidl_sme::ConnectResultCode::Failed, denial.reason);
}
}
#[test]
fn test_credential_from_bytes() {
assert_eq!(Credential::from_bytes(vec![1]), Credential::Password(vec![1]));
assert_eq!(Credential::from_bytes(vec![2; 63]), Credential::Password(vec![2; 63]));
assert_eq!(Credential::from_bytes(vec![2; 64]), Credential::Psk(vec![2; 64]));
assert_eq!(Credential::from_bytes(vec![]), Credential::None);
}
#[test]
fn test_derived_security_type_from_credential() {
let password = Credential::Password(b"password".to_vec());
let psk = Credential::Psk(b"psk-type".to_vec());
let none = Credential::None;
assert_eq!(SecurityType::Wpa2, password.derived_security_type());
assert_eq!(SecurityType::Wpa2, psk.derived_security_type());
assert_eq!(SecurityType::None, none.derived_security_type());
}
}