| // 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()); |
| } |
| } |