use std::fmt;
use serde::{Deserialize, Serialize};
use super::access_point::{AccessPoint, SecurityFeatures};
use super::error::ConnectionError;
use super::saved_connection::SavedConnectionBrief;
use super::{Redacted, redact_option};
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct WifiNetworkGroup {
pub ssid: String,
pub interface: String,
pub strongest: AccessPoint,
pub access_points: Vec<AccessPoint>,
pub saved_profiles: Vec<SavedConnectionBrief>,
pub active: bool,
pub known: bool,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Network {
pub device: String,
pub ssid: String,
pub bssid: Option<String>,
pub strength: Option<u8>,
pub frequency: Option<u32>,
pub secured: bool,
pub is_psk: bool,
pub is_eap: bool,
pub is_hotspot: bool,
pub ip4_address: Option<String>,
pub ip6_address: Option<String>,
#[serde(default)]
pub best_bssid: String,
#[serde(default)]
pub bssids: Vec<String>,
#[serde(default)]
pub is_active: bool,
#[serde(default)]
pub known: bool,
#[serde(default)]
pub security_features: SecurityFeatures,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkInfo {
pub ssid: String,
pub bssid: String,
pub strength: u8,
pub freq: Option<u32>,
pub channel: Option<u16>,
pub mode: String,
pub rate_mbps: Option<u32>,
pub bars: String,
pub security: String,
pub status: String,
pub ip4_address: Option<String>,
pub ip6_address: Option<String>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EapMethod {
Peap,
Ttls,
Tls,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Phase2 {
Mschapv2,
Pap,
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq)]
pub struct EapOptions {
pub identity: String,
pub password: String,
pub anonymous_identity: Option<String>,
pub domain_suffix_match: Option<String>,
pub ca_cert_path: Option<String>,
pub ca_cert_blob: Option<Vec<u8>>,
pub system_ca_certs: bool,
pub method: EapMethod,
pub phase2: Phase2,
pub private_key_path: Option<String>,
pub private_key_blob: Option<Vec<u8>>,
pub private_key_password: Option<String>,
pub client_cert_path: Option<String>,
pub client_cert_blob: Option<Vec<u8>>,
}
impl fmt::Debug for EapOptions {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("EapOptions")
.field("identity", &self.identity)
.field("password", &Redacted)
.field("anonymous_identity", &self.anonymous_identity)
.field("domain_suffix_match", &self.domain_suffix_match)
.field("ca_cert_path", &self.ca_cert_path)
.field("ca_cert_blob", &self.ca_cert_blob)
.field("system_ca_certs", &self.system_ca_certs)
.field("method", &self.method)
.field("phase2", &self.phase2)
.field("private_key_path", &self.private_key_path)
.field("private_key_blob", &redact_option(&self.private_key_blob))
.field(
"private_key_password",
&redact_option(&self.private_key_password),
)
.field("client_cert_path", &self.client_cert_path)
.field("client_cert_blob", &self.client_cert_blob)
.finish()
}
}
impl Default for EapOptions {
fn default() -> Self {
Self {
identity: String::new(),
password: String::new(),
anonymous_identity: None,
domain_suffix_match: None,
ca_cert_path: None,
ca_cert_blob: None,
system_ca_certs: false,
method: EapMethod::Peap,
phase2: Phase2::Mschapv2,
private_key_path: None,
private_key_blob: None,
private_key_password: None,
client_cert_path: None,
client_cert_blob: None,
}
}
}
impl EapOptions {
pub fn new(identity: impl Into<String>, password: impl Into<String>) -> Self {
Self {
identity: identity.into(),
password: password.into(),
..Default::default()
}
}
pub fn new_tls_path(
identity: impl Into<String>,
private_key_path: impl Into<String>,
client_cert_path: impl Into<String>,
) -> Self {
Self {
identity: identity.into(),
method: EapMethod::Tls,
private_key_path: Some(private_key_path.into()),
client_cert_path: Some(client_cert_path.into()),
..Default::default()
}
}
pub fn new_tls_blob(
identity: impl Into<String>,
private_key_blob: impl Into<Vec<u8>>,
client_cert_blob: impl Into<Vec<u8>>,
) -> Self {
Self {
identity: identity.into(),
method: EapMethod::Tls,
private_key_blob: Some(private_key_blob.into()),
client_cert_blob: Some(client_cert_blob.into()),
..Default::default()
}
}
#[must_use]
pub fn builder() -> EapOptionsBuilder {
EapOptionsBuilder::default()
}
#[must_use]
pub fn with_anonymous_identity(mut self, anonymous_identity: impl Into<String>) -> Self {
self.anonymous_identity = Some(anonymous_identity.into());
self
}
#[must_use]
pub fn with_domain_suffix_match(mut self, domain: impl Into<String>) -> Self {
self.domain_suffix_match = Some(domain.into());
self
}
#[must_use]
pub fn with_ca_cert_path(mut self, path: impl Into<String>) -> Self {
self.ca_cert_blob = None;
self.ca_cert_path = Some(path.into());
self
}
#[must_use]
pub fn with_ca_cert_blob(mut self, data: impl Into<Vec<u8>>) -> Self {
self.ca_cert_path = None;
self.ca_cert_blob = Some(data.into());
self
}
#[must_use]
pub fn with_system_ca_certs(mut self, use_system: bool) -> Self {
self.system_ca_certs = use_system;
self
}
#[must_use]
pub fn with_method(mut self, method: EapMethod) -> Self {
self.method = method;
self
}
#[must_use]
pub fn with_phase2(mut self, phase2: Phase2) -> Self {
self.phase2 = phase2;
self
}
#[must_use]
pub fn with_private_key_password(mut self, password: impl Into<String>) -> Self {
self.private_key_password = Some(password.into());
self
}
}
#[derive(Default)]
pub struct EapOptionsBuilder {
identity: Option<String>,
password: Option<String>,
anonymous_identity: Option<String>,
domain_suffix_match: Option<String>,
ca_cert_path: Option<String>,
ca_cert_blob: Option<Vec<u8>>,
system_ca_certs: bool,
method: Option<EapMethod>,
phase2: Option<Phase2>,
private_key_path: Option<String>,
private_key_blob: Option<Vec<u8>>,
private_key_password: Option<String>,
client_cert_path: Option<String>,
client_cert_blob: Option<Vec<u8>>,
}
impl fmt::Debug for EapOptionsBuilder {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("EapOptionsBuilder")
.field("identity", &self.identity)
.field("password", &redact_option(&self.password))
.field("anonymous_identity", &self.anonymous_identity)
.field("domain_suffix_match", &self.domain_suffix_match)
.field("ca_cert_path", &self.ca_cert_path)
.field("ca_cert_blob", &self.ca_cert_blob)
.field("system_ca_certs", &self.system_ca_certs)
.field("method", &self.method)
.field("phase2", &self.phase2)
.field("private_key_path", &self.private_key_path)
.field("private_key_blob", &redact_option(&self.private_key_blob))
.field(
"private_key_password",
&redact_option(&self.private_key_password),
)
.field("client_cert_path", &self.client_cert_path)
.field("client_cert_blob", &self.client_cert_blob)
.finish()
}
}
impl EapOptionsBuilder {
#[must_use]
pub fn identity(mut self, identity: impl Into<String>) -> Self {
self.identity = Some(identity.into());
self
}
#[must_use]
pub fn password(mut self, password: impl Into<String>) -> Self {
self.password = Some(password.into());
self
}
#[must_use]
pub fn anonymous_identity(mut self, anonymous_identity: impl Into<String>) -> Self {
self.anonymous_identity = Some(anonymous_identity.into());
self
}
#[must_use]
pub fn domain_suffix_match(mut self, domain: impl Into<String>) -> Self {
self.domain_suffix_match = Some(domain.into());
self
}
#[must_use]
pub fn ca_cert_path(mut self, path: impl Into<String>) -> Self {
self.ca_cert_blob = None;
self.ca_cert_path = Some(path.into());
self
}
#[must_use]
pub fn ca_cert_blob(mut self, data: impl Into<Vec<u8>>) -> Self {
self.ca_cert_path = None;
self.ca_cert_blob = Some(data.into());
self
}
#[must_use]
pub fn system_ca_certs(mut self, use_system: bool) -> Self {
self.system_ca_certs = use_system;
self
}
#[must_use]
pub fn method(mut self, method: EapMethod) -> Self {
self.method = Some(method);
self
}
#[must_use]
pub fn phase2(mut self, phase2: Phase2) -> Self {
self.phase2 = Some(phase2);
self
}
#[must_use]
pub fn private_key_path(mut self, path: impl Into<String>) -> Self {
self.private_key_blob = None;
self.private_key_path = Some(path.into());
self
}
#[must_use]
pub fn private_key_blob(mut self, data: impl Into<Vec<u8>>) -> Self {
self.private_key_path = None;
self.private_key_blob = Some(data.into());
self
}
#[must_use]
pub fn private_key_password(mut self, password: impl Into<String>) -> Self {
self.private_key_password = Some(password.into());
self
}
#[must_use]
pub fn client_cert_path(mut self, path: impl Into<String>) -> Self {
self.client_cert_blob = None;
self.client_cert_path = Some(path.into());
self
}
#[must_use]
pub fn client_cert_blob(mut self, data: impl Into<Vec<u8>>) -> Self {
self.client_cert_path = None;
self.client_cert_blob = Some(data.into());
self
}
#[must_use = "use the EAP options with WifiSecurity::WpaEap or handle the error"]
pub fn build(self) -> Result<EapOptions, ConnectionError> {
let is_peap_or_ttls =
self.method == Some(EapMethod::Peap) || self.method == Some(EapMethod::Ttls);
if let (Some(_), Some(_)) = (&self.ca_cert_path, &self.ca_cert_blob) {
return Err(ConnectionError::IncompleteBuilder(
"EAP CA certificate cannot be specified both as a path and blob".into(),
));
}
match (&self.method, &self.private_key_path, &self.private_key_blob) {
(_, Some(_), Some(_)) => {
return Err(ConnectionError::IncompleteBuilder(
"EAP private key cannot be specified both as a path and blob".into(),
));
}
(Some(EapMethod::Tls), None, None) => {
return Err(ConnectionError::IncompleteBuilder(
"EAP private key is required for TLS (use .private_key_path() or .private_key_blob())".into(),
));
}
_ => {}
}
match (&self.method, &self.client_cert_path, &self.client_cert_blob) {
(_, Some(_), Some(_)) => {
return Err(ConnectionError::IncompleteBuilder(
"EAP client certificate cannot be specified both as a path and blob".into(),
));
}
(Some(EapMethod::Tls), None, None) => {
return Err(ConnectionError::IncompleteBuilder(
"EAP client certificate is required for TLS (use .client_cert_path() or .client_cert_blob())".into(),
));
}
_ => {}
}
Ok(EapOptions {
identity: self.identity.ok_or_else(|| {
ConnectionError::IncompleteBuilder(
"EAP identity is required (use .identity())".into(),
)
})?,
password: if is_peap_or_ttls {
self.password.ok_or_else(|| {
ConnectionError::IncompleteBuilder(
"EAP password is required (use .password())".into(),
)
})?
} else {
String::new()
},
anonymous_identity: self.anonymous_identity,
domain_suffix_match: self.domain_suffix_match,
ca_cert_path: self.ca_cert_path,
ca_cert_blob: self.ca_cert_blob,
system_ca_certs: self.system_ca_certs,
method: self.method.ok_or_else(|| {
ConnectionError::IncompleteBuilder("EAP method is required (use .method())".into())
})?,
phase2: if is_peap_or_ttls {
self.phase2.ok_or_else(|| {
ConnectionError::IncompleteBuilder(
"EAP phase 2 method is required (use .phase2())".into(),
)
})?
} else {
Phase2::Mschapv2
},
private_key_path: self.private_key_path,
private_key_blob: self.private_key_blob,
private_key_password: self.private_key_password,
client_cert_path: self.client_cert_path,
client_cert_blob: self.client_cert_blob,
})
}
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq)]
pub enum WifiSecurity {
Open,
WpaPsk {
psk: String,
},
WpaEap {
opts: EapOptions,
},
Wpa3Eap192bit {
opts: EapOptions,
},
}
impl fmt::Debug for WifiSecurity {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Open => formatter.write_str("Open"),
Self::WpaPsk { .. } => formatter
.debug_struct("WpaPsk")
.field("psk", &Redacted)
.finish(),
Self::WpaEap { opts } => formatter
.debug_struct("WpaEap")
.field("opts", opts)
.finish(),
Self::Wpa3Eap192bit { opts } => formatter
.debug_struct("Wpa3Eap192bit")
.field("opts", opts)
.finish(),
}
}
}
impl WifiSecurity {
#[must_use]
pub fn secured(&self) -> bool {
!matches!(self, WifiSecurity::Open)
}
#[must_use]
pub fn is_psk(&self) -> bool {
matches!(self, WifiSecurity::WpaPsk { .. })
}
#[must_use]
pub fn is_eap(&self) -> bool {
matches!(
self,
WifiSecurity::WpaEap { .. } | WifiSecurity::Wpa3Eap192bit { .. }
)
}
}
impl Network {
pub fn merge_ap(&mut self, other: &Network) {
let mut bssids = self.bssids.clone();
if let Some(bssid) = &self.bssid {
push_unique_bssid(&mut bssids, bssid);
}
for bssid in &other.bssids {
push_unique_bssid(&mut bssids, bssid);
}
if let Some(bssid) = &other.bssid {
push_unique_bssid(&mut bssids, bssid);
}
if other.strength.unwrap_or(0) > self.strength.unwrap_or(0) {
self.strength = other.strength;
self.frequency = other.frequency;
self.bssid = other.bssid.clone();
self.best_bssid = other.best_bssid.clone();
}
if let Some(best_bssid) = &self.bssid {
bssids.retain(|bssid| !bssid.eq_ignore_ascii_case(best_bssid));
bssids.insert(0, best_bssid.clone());
}
self.bssids = bssids;
merge_security_features(&mut self.security_features, other.security_features);
self.secured |= other.secured;
self.is_psk |= other.is_psk;
self.is_eap |= other.is_eap;
self.is_hotspot |= other.is_hotspot;
self.is_active |= other.is_active;
self.known |= other.known;
if self.ip4_address.is_none() {
self.ip4_address.clone_from(&other.ip4_address);
}
if self.ip6_address.is_none() {
self.ip6_address.clone_from(&other.ip6_address);
}
if self.device.is_empty() {
self.device.clone_from(&other.device);
}
}
}
fn push_unique_bssid(bssids: &mut Vec<String>, candidate: &str) {
if !bssids
.iter()
.any(|bssid| bssid.eq_ignore_ascii_case(candidate))
{
bssids.push(candidate.to_string());
}
}
fn merge_security_features(current: &mut SecurityFeatures, other: SecurityFeatures) {
current.privacy |= other.privacy;
current.wps |= other.wps;
current.psk |= other.psk;
current.eap |= other.eap;
current.sae |= other.sae;
current.owe |= other.owe;
current.owe_transition_mode |= other.owe_transition_mode;
current.eap_suite_b_192 |= other.eap_suite_b_192;
current.wep40 |= other.wep40;
current.wep104 |= other.wep104;
current.tkip |= other.tkip;
current.ccmp |= other.ccmp;
}
#[cfg(test)]
mod network_merge_tests {
use super::{Network, SecurityFeatures};
fn network(bssid: &str, strength: u8) -> Network {
Network {
device: "wlan0".into(),
ssid: "net".into(),
bssid: Some(bssid.into()),
strength: Some(strength),
frequency: Some(2412),
secured: false,
is_psk: false,
is_eap: false,
is_hotspot: false,
ip4_address: None,
ip6_address: None,
best_bssid: bssid.into(),
bssids: vec![bssid.into()],
is_active: false,
known: false,
security_features: SecurityFeatures::default(),
}
}
#[test]
fn merge_ap_keeps_ip_and_device_when_stronger_ap_has_none() {
let mut weaker_connected = Network {
device: "wlan0".into(),
ssid: "net".into(),
bssid: Some("aa:aa:aa:aa:aa:aa".into()),
strength: Some(20),
frequency: Some(5200),
secured: true,
is_psk: true,
is_eap: false,
is_hotspot: false,
ip4_address: Some("192.168.1.5/24".into()),
ip6_address: Some("fe80::1/64".into()),
best_bssid: "aa:aa:aa:aa:aa:aa".into(),
bssids: vec!["aa:aa:aa:aa:aa:aa".into()],
is_active: true,
known: false,
security_features: Default::default(),
};
weaker_connected.security_features.psk = true;
weaker_connected.security_features.ccmp = true;
let mut stronger = Network {
device: String::new(),
ssid: "net".into(),
bssid: Some("bb:bb:bb:bb:bb:bb".into()),
strength: Some(90),
frequency: Some(5200),
secured: true,
is_psk: true,
is_eap: false,
is_hotspot: false,
ip4_address: None,
ip6_address: None,
best_bssid: "bb:bb:bb:bb:bb:bb".into(),
bssids: vec!["bb:bb:bb:bb:bb:bb".into()],
is_active: false,
known: false,
security_features: Default::default(),
};
stronger.security_features.eap = true;
stronger.security_features.sae = true;
weaker_connected.merge_ap(&stronger);
assert_eq!(weaker_connected.strength, Some(90));
assert_eq!(weaker_connected.bssid, Some("bb:bb:bb:bb:bb:bb".into()));
assert_eq!(weaker_connected.best_bssid, "bb:bb:bb:bb:bb:bb");
assert_eq!(weaker_connected.ip4_address, Some("192.168.1.5/24".into()));
assert_eq!(weaker_connected.ip6_address, Some("fe80::1/64".into()));
assert_eq!(weaker_connected.device, "wlan0");
assert!(weaker_connected.is_active);
assert!(weaker_connected.security_features.psk);
assert!(weaker_connected.security_features.ccmp);
assert!(weaker_connected.security_features.eap);
assert!(weaker_connected.security_features.sae);
assert_eq!(
weaker_connected.bssids,
vec![
"bb:bb:bb:bb:bb:bb".to_string(),
"aa:aa:aa:aa:aa:aa".to_string()
]
);
}
#[test]
fn merge_ap_combines_flags_security_and_all_unique_bssids() {
let mut strongest = network("AA:AA:AA:AA:AA:01", 90);
strongest.frequency = Some(5180);
strongest.secured = true;
strongest.is_psk = true;
strongest.security_features.psk = true;
strongest.security_features.ccmp = true;
let mut weaker = network("BB:BB:BB:BB:BB:01", 30);
weaker.frequency = Some(2412);
weaker.is_eap = true;
weaker.is_hotspot = true;
weaker.known = true;
weaker.bssids = vec![
"BB:BB:BB:BB:BB:01".into(),
"CC:CC:CC:CC:CC:01".into(),
"aa:aa:aa:aa:aa:01".into(),
];
weaker.security_features.eap = true;
weaker.security_features.sae = true;
weaker.security_features.wps = true;
strongest.merge_ap(&weaker);
assert_eq!(strongest.strength, Some(90));
assert_eq!(strongest.frequency, Some(5180));
assert_eq!(strongest.bssid.as_deref(), Some("AA:AA:AA:AA:AA:01"));
assert_eq!(
strongest.bssids,
vec![
"AA:AA:AA:AA:AA:01".to_string(),
"BB:BB:BB:BB:BB:01".to_string(),
"CC:CC:CC:CC:CC:01".to_string(),
]
);
assert!(strongest.secured);
assert!(strongest.is_psk);
assert!(strongest.is_eap);
assert!(strongest.is_hotspot);
assert!(strongest.known);
assert!(strongest.security_features.psk);
assert!(strongest.security_features.eap);
assert!(strongest.security_features.sae);
assert!(strongest.security_features.wps);
assert!(strongest.security_features.ccmp);
}
#[test]
fn merge_ap_fills_missing_connection_context() {
let mut network_without_context = network("AA:AA:AA:AA:AA:01", 80);
network_without_context.device.clear();
let mut connected = network("BB:BB:BB:BB:BB:01", 40);
connected.device = "wlan1".into();
connected.ip4_address = Some("192.168.50.5/24".into());
connected.ip6_address = Some("2001:db8::5/64".into());
connected.is_active = true;
network_without_context.merge_ap(&connected);
assert_eq!(network_without_context.device, "wlan1");
assert_eq!(
network_without_context.ip4_address.as_deref(),
Some("192.168.50.5/24")
);
assert_eq!(
network_without_context.ip6_address.as_deref(),
Some("2001:db8::5/64")
);
assert!(network_without_context.is_active);
}
}