1use crate::client::scan::{self, ScanReason};
6use crate::client::types::{
7 self, Bss, InternalSavedNetworkData, SecurityType, SecurityTypeDetailed,
8};
9use crate::config_management::{
10 self, Credential, SavedNetworksManagerApi, network_config, select_authentication_method,
11 select_subset_potentially_hidden_networks,
12};
13use crate::telemetry::{self, TelemetryEvent, TelemetrySender};
14use anyhow::format_err;
15use async_trait::async_trait;
16use fidl_fuchsia_wlan_common as fidl_common;
17use fuchsia_async as fasync;
18use fuchsia_inspect::Node as InspectNode;
19use fuchsia_inspect_contrib::inspect_insert;
20use fuchsia_inspect_contrib::log::WriteInspect;
21use fuchsia_inspect_contrib::nodes::BoundedListNode as InspectBoundedListNode;
22use futures::channel::{mpsc, oneshot};
23use futures::lock::Mutex;
24use futures::select;
25use futures::stream::StreamExt;
26use log::{debug, error, info, warn};
27use std::borrow::Cow;
28use std::collections::{HashMap, HashSet};
29use std::rc::Rc;
30use std::sync::Arc;
31use wlan_common::security::SecurityAuthenticator;
32use wlan_common::sequestered::Sequestered;
33
34pub mod bss_selection;
35pub mod fut_manager;
36pub mod network_selection;
37pub mod scoring_functions;
38
39pub const CONNECTION_SELECTION_REQUEST_BUFFER_SIZE: usize = 100;
40const INSPECT_EVENT_LIMIT_FOR_CONNECTION_SELECTIONS: usize = 10;
41
42const RECENT_DISCONNECT_WINDOW: zx::MonotonicDuration =
43 zx::MonotonicDuration::from_seconds(60 * 15);
44const RECENT_FAILURE_WINDOW: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(60 * 5);
45const SHORT_CONNECT_DURATION: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(7 * 60);
46
47#[derive(Clone)]
48pub struct ConnectionSelectionRequester {
49 sender: mpsc::Sender<ConnectionSelectionRequest>,
50}
51
52impl ConnectionSelectionRequester {
53 pub fn new(sender: mpsc::Sender<ConnectionSelectionRequest>) -> Self {
54 Self { sender }
55 }
56
57 pub async fn do_connection_selection(
58 &mut self,
59 network_id: Option<types::NetworkIdentifier>,
61 reason: types::ConnectReason,
62 ) -> Result<Option<types::ScannedCandidate>, anyhow::Error> {
63 let (sender, receiver) = oneshot::channel();
64 self.sender
65 .try_send(ConnectionSelectionRequest::NewConnectionSelection {
66 network_id,
67 reason,
68 responder: sender,
69 })
70 .map_err(|e| format_err!("Failed to send connection selection request: {}", e))?;
71 receiver.await.map_err(|e| format_err!("Error during connection selection: {:?}", e))
72 }
73 pub async fn do_roam_selection(
74 &mut self,
75 scan_type: fidl_common::ScanType,
76 network_id: types::NetworkIdentifier,
77 credential: network_config::Credential,
78 current_security: types::SecurityTypeDetailed,
79 ) -> Result<Option<types::ScannedCandidate>, anyhow::Error> {
80 let (sender, receiver) = oneshot::channel();
81 self.sender
82 .try_send(ConnectionSelectionRequest::RoamSelection {
83 scan_type,
84 network_id,
85 credential,
86 current_security,
87 responder: sender,
88 })
89 .map_err(|e| format_err!("Failed to queue connection selection: {}", e))?;
90 receiver.await.map_err(|e| format_err!("Error during roam selection: {:?}", e))
91 }
92}
93
94#[cfg_attr(test, derive(Debug))]
95pub enum ConnectionSelectionRequest {
96 NewConnectionSelection {
97 network_id: Option<types::NetworkIdentifier>,
98 reason: types::ConnectReason,
99 responder: oneshot::Sender<Option<types::ScannedCandidate>>,
100 },
101 RoamSelection {
102 scan_type: fidl_common::ScanType,
103 network_id: types::NetworkIdentifier,
104 credential: network_config::Credential,
105 current_security: types::SecurityTypeDetailed,
106 responder: oneshot::Sender<Option<types::ScannedCandidate>>,
107 },
108}
109
110#[async_trait(?Send)]
112pub trait ConnectionSelectorApi {
113 async fn find_and_select_connection_candidate(
115 &self,
116 network: Option<types::NetworkIdentifier>,
117 reason: types::ConnectReason,
118 ) -> Option<types::ScannedCandidate>;
119 async fn find_and_select_roam_candidate(
121 &self,
122 scan_type: fidl_common::ScanType,
123 network: types::NetworkIdentifier,
124 credential: &network_config::Credential,
125 current_security: types::SecurityTypeDetailed,
126 ) -> Option<types::ScannedCandidate>;
127}
128
129pub async fn serve_connection_selection_request_loop(
131 connection_selector: Rc<dyn ConnectionSelectorApi>,
132 mut request_channel: mpsc::Receiver<ConnectionSelectionRequest>,
133 power_manager: Arc<dyn wlan_power_manager::PowerManager>,
134) {
135 loop {
136 select! {
137 request = request_channel.select_next_some() => {
138 match request {
139 ConnectionSelectionRequest::NewConnectionSelection { network_id, reason, responder} => {
140 let _wake_lease = power_manager.take_wake_lease("wlancfg-connection-selection").await;
141 let selected = connection_selector.find_and_select_connection_candidate(network_id, reason).await;
142 let _ = responder.send(selected);
145 }
146 ConnectionSelectionRequest::RoamSelection { scan_type, network_id, credential, current_security, responder } => {
147 let _wake_lease = power_manager.take_wake_lease("wlancfg-roam-selection").await;
148 let selected = connection_selector.find_and_select_roam_candidate(scan_type, network_id, &credential, current_security).await;
149 let _ = responder.send(selected);
152 }
153 }
154 }
155 }
156 }
157}
158
159pub struct ConnectionSelector {
160 saved_network_manager: Arc<dyn SavedNetworksManagerApi>,
161 scan_requester: Arc<dyn scan::ScanRequestApi>,
162 last_scan_result_time: Arc<Mutex<zx::MonotonicInstant>>,
163 _inspect_node_root: Arc<Mutex<InspectNode>>,
164 inspect_node_for_connection_selection: Arc<Mutex<InspectBoundedListNode>>,
165 telemetry_sender: TelemetrySender,
166}
167
168impl ConnectionSelector {
169 pub fn new(
170 saved_network_manager: Arc<dyn SavedNetworksManagerApi>,
171 scan_requester: Arc<dyn scan::ScanRequestApi>,
172 inspect_node: InspectNode,
173 telemetry_sender: TelemetrySender,
174 ) -> Self {
175 let inspect_node_for_connection_selection = InspectBoundedListNode::new(
176 inspect_node.create_child("connection_selection"),
177 INSPECT_EVENT_LIMIT_FOR_CONNECTION_SELECTIONS,
178 );
179 Self {
180 saved_network_manager,
181 scan_requester,
182 last_scan_result_time: Arc::new(Mutex::new(zx::MonotonicInstant::ZERO)),
183 _inspect_node_root: Arc::new(Mutex::new(inspect_node)),
184 inspect_node_for_connection_selection: Arc::new(Mutex::new(
185 inspect_node_for_connection_selection,
186 )),
187 telemetry_sender,
188 }
189 }
190
191 async fn find_available_bss_candidate_list(
194 &self,
195 network: Option<types::NetworkIdentifier>,
196 ) -> Vec<types::ScannedCandidate> {
197 let scan_for_candidates = || async {
198 if let Some(network) = &network {
199 self.scan_requester
200 .perform_scan(ScanReason::BssSelection, vec![network.ssid.clone()], vec![])
201 .await
202 } else {
203 let last_scan_result_time = *self.last_scan_result_time.lock().await;
204 let scan_age = zx::MonotonicInstant::get() - last_scan_result_time;
205 if last_scan_result_time != zx::MonotonicInstant::ZERO {
206 info!("Scan results are {}s old, triggering a scan", scan_age.into_seconds());
207 self.telemetry_sender.send(TelemetryEvent::NetworkSelectionScanInterval {
208 time_since_last_scan: scan_age,
209 });
210 }
211 let passive_scan_results = match self
212 .scan_requester
213 .perform_scan(ScanReason::NetworkSelection, vec![], vec![])
214 .await
215 {
216 Ok(scan_results) => scan_results,
217 Err(e) => return Err(e),
218 };
219 let passive_scan_ssids: HashSet<types::Ssid> = HashSet::from_iter(
220 passive_scan_results.iter().map(|result| result.ssid.clone()),
221 );
222 let requested_active_scan_ssids: Vec<types::Ssid> =
223 select_subset_potentially_hidden_networks(
224 self.saved_network_manager.get_networks().await,
225 )
226 .drain(..)
227 .map(|id| id.ssid)
228 .filter(|ssid| !passive_scan_ssids.contains(ssid))
229 .collect();
230
231 self.telemetry_sender.send(TelemetryEvent::ActiveScanRequested {
232 num_ssids_requested: requested_active_scan_ssids.len(),
233 });
234
235 if requested_active_scan_ssids.is_empty() {
236 Ok(passive_scan_results)
237 } else {
238 self.scan_requester
239 .perform_scan(
240 ScanReason::NetworkSelection,
241 requested_active_scan_ssids,
242 vec![],
243 )
244 .await
245 .map(|mut scan_results| {
246 scan_results.extend(passive_scan_results);
247 scan_results
248 })
249 }
250 }
251 };
252
253 let scan_results = scan_for_candidates().await;
254
255 match scan_results {
256 Err(e) => {
257 warn!("Failed to get available BSSs, {:?}", e);
258 vec![]
259 }
260 Ok(scan_results) => {
261 let candidates =
262 merge_saved_networks_and_scan_data(&self.saved_network_manager, scan_results)
263 .await;
264 if network.is_none() {
265 *self.last_scan_result_time.lock().await = zx::MonotonicInstant::get();
266 record_metrics_on_scan(candidates.clone(), &self.telemetry_sender);
267 }
268 candidates
269 }
270 }
271 }
272
273 async fn augment_bss_candidate_with_active_scan(
276 &self,
277 scanned_candidate: types::ScannedCandidate,
278 ) -> types::ScannedCandidate {
279 async fn get_enhanced_bss_description(
282 scanned_candidate: &types::ScannedCandidate,
283 scan_requester: Arc<dyn scan::ScanRequestApi>,
284 ) -> Result<Sequestered<fidl_fuchsia_wlan_ieee80211::BssDescription>, ()> {
285 match scanned_candidate.bss.observation {
286 types::ScanObservation::Passive => {
287 info!("Performing directed active scan on selected network")
288 }
289 types::ScanObservation::Active => {
290 debug!("Network already discovered via active scan.");
291 return Err(());
292 }
293 types::ScanObservation::Unknown => {
294 error!("Unexpected `Unknown` variant of network `observation`.");
295 return Err(());
296 }
297 }
298
299 let mut directed_scan_result = scan_requester
301 .perform_scan(
302 ScanReason::BssSelectionAugmentation,
303 vec![scanned_candidate.network.ssid.clone()],
304 vec![scanned_candidate.bss.channel],
305 )
306 .await
307 .map_err(|_| {
308 info!("Failed to perform active scan to augment BSS info.");
309 })?;
310
311 let bss_description = directed_scan_result
313 .drain(..)
314 .find_map(|mut network| {
315 if network.ssid == scanned_candidate.network.ssid {
316 for bss in network.entries.drain(..) {
317 if bss.bssid == scanned_candidate.bss.bssid {
318 return Some(bss.bss_description);
319 }
320 }
321 }
322 None
323 })
324 .ok_or_else(|| {
325 info!("BSS info will lack active scan augmentation, proceeding anyway.");
326 })?;
327
328 Ok(bss_description)
329 }
330
331 match get_enhanced_bss_description(&scanned_candidate, self.scan_requester.clone()).await {
332 Ok(new_bss_description) => {
333 let updated_scanned_bss =
334 Bss { bss_description: new_bss_description, ..scanned_candidate.bss.clone() };
335 types::ScannedCandidate { bss: updated_scanned_bss, ..scanned_candidate }
336 }
337 Err(()) => scanned_candidate,
338 }
339 }
340
341 async fn roam_scan(
343 &self,
344 scan_type: fidl_common::ScanType,
345 network: types::NetworkIdentifier,
346 current_security: types::SecurityTypeDetailed,
347 ) -> Vec<types::ScanResult> {
348 let ssids = match scan_type {
349 fidl_common::ScanType::Passive => vec![],
350 fidl_common::ScanType::Active => vec![network.ssid.clone()],
351 };
352 self.scan_requester
353 .perform_scan(ScanReason::RoamSearch, ssids, vec![])
354 .await
355 .unwrap_or_else(|e| {
356 error!("{}", format_err!("Error scanning: {:?}", e));
357 vec![]
358 })
359 .into_iter()
360 .filter(|s| {
361 s.ssid == network.ssid && current_security == s.security_type_detailed
363 })
364 .collect::<Vec<_>>()
365 }
366}
367
368#[async_trait(?Send)]
369impl ConnectionSelectorApi for ConnectionSelector {
370 async fn find_and_select_connection_candidate(
375 &self,
376 network: Option<types::NetworkIdentifier>,
377 reason: types::ConnectReason,
378 ) -> Option<types::ScannedCandidate> {
379 let available_candidate_list =
381 self.find_available_bss_candidate_list(network.clone()).await;
382
383 let available_networks: HashSet<types::NetworkIdentifier> =
385 available_candidate_list.iter().map(|candidate| candidate.network.clone()).collect();
386 let selected_networks = network_selection::select_networks(available_networks, &network);
387
388 self.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
390 network_selection_type: match network {
391 Some(_) => telemetry::NetworkSelectionType::Directed,
392 None => telemetry::NetworkSelectionType::Undirected,
393 },
394 num_candidates: (!available_candidate_list.is_empty())
395 .then_some(available_candidate_list.len())
396 .ok_or(()),
397 selected_count: selected_networks.len(),
398 });
399
400 let allowed_candidate_list = available_candidate_list
402 .iter()
403 .filter(|candidate| selected_networks.contains(&candidate.network))
404 .cloned()
405 .collect();
406
407 match bss_selection::select_bss(
410 allowed_candidate_list,
411 reason,
412 self.inspect_node_for_connection_selection.clone(),
413 self.telemetry_sender.clone(),
414 )
415 .await
416 {
417 Some(mut candidate) => {
418 if network.is_some() {
419 candidate.bss.observation = types::ScanObservation::Unknown;
423 }
424 match candidate.bss.observation {
426 types::ScanObservation::Passive => {
427 Some(self.augment_bss_candidate_with_active_scan(candidate.clone()).await)
428 }
429 _ => Some(candidate),
430 }
431 }
432 None => None,
433 }
434 }
435
436 async fn find_and_select_roam_candidate(
440 &self,
441 scan_type: fidl_common::ScanType,
442 network: types::NetworkIdentifier,
443 credential: &network_config::Credential,
444 current_security: types::SecurityTypeDetailed,
445 ) -> Option<types::ScannedCandidate> {
446 let mut matching_scan_results =
448 self.roam_scan(scan_type, network.clone(), current_security).await;
449 if matching_scan_results.is_empty() && scan_type == fidl_common::ScanType::Passive {
450 info!("No scan results seen in passive roam scan. Active scanning.");
451 matching_scan_results = self
452 .roam_scan(fidl_common::ScanType::Active, network.clone(), current_security)
453 .await;
454 }
455
456 let mut candidates = Vec::new();
457 for mut s in matching_scan_results {
460 if let Some(config) = self
461 .saved_network_manager
462 .lookup(&network)
463 .await
464 .filter(|c| &c.credential == credential)
465 {
466 candidates.append(&mut merge_config_and_scan_data(config, &mut s));
467 } else {
468 warn!("Failed to find config for network to roam from");
471 }
472 }
473 bss_selection::select_bss(
475 candidates,
476 types::ConnectReason::ProactiveNetworkSwitch,
477 self.inspect_node_for_connection_selection.clone(),
478 self.telemetry_sender.clone(),
479 )
480 .await
481 }
482}
483
484impl types::ScannedCandidate {
485 pub fn recent_failure_count(&self) -> u64 {
486 self.saved_network_info
487 .recent_failures
488 .iter()
489 .filter(|failure| failure.bssid == self.bss.bssid)
490 .count()
491 .try_into()
492 .unwrap_or_else(|e| {
493 error!("{}", e);
494 u64::MAX
495 })
496 }
497 pub fn recent_short_connections(&self) -> usize {
498 self.saved_network_info
499 .past_connections
500 .get(&self.bss.bssid)
501 .cloned()
502 .unwrap_or_default()
503 .get_recent(fasync::MonotonicInstant::now() - RECENT_DISCONNECT_WINDOW)
504 .iter()
505 .filter(|d| d.connection_uptime < SHORT_CONNECT_DURATION)
506 .count()
507 }
508
509 pub fn saved_security_type_to_string(&self) -> String {
510 match self.network.security_type {
511 SecurityType::None => "open",
512 SecurityType::Wep => "WEP",
513 SecurityType::Wpa => "WPA1",
514 SecurityType::Wpa2 => "WPA2",
515 SecurityType::Wpa3 => "WPA3",
516 }
517 .to_string()
518 }
519
520 pub fn scanned_security_type_to_string(&self) -> String {
521 match self.security_type_detailed {
522 SecurityTypeDetailed::Unknown => "unknown",
523 SecurityTypeDetailed::Open => "open",
524 SecurityTypeDetailed::OpenOweTransition => "Open OWE Transition",
525 SecurityTypeDetailed::Owe => "OWE",
526 SecurityTypeDetailed::Wep => "WEP",
527 SecurityTypeDetailed::Wpa1 => "WPA1",
528 SecurityTypeDetailed::Wpa1Wpa2PersonalTkipOnly => "WPA1/2Tk",
529 SecurityTypeDetailed::Wpa2PersonalTkipOnly => "WPA2Tk",
530 SecurityTypeDetailed::Wpa1Wpa2Personal => "WPA1/2",
531 SecurityTypeDetailed::Wpa2Personal => "WPA2",
532 SecurityTypeDetailed::Wpa2Wpa3Personal => "WPA2/3",
533 SecurityTypeDetailed::Wpa3Personal => "WPA3",
534 SecurityTypeDetailed::Wpa2Enterprise => "WPA2Ent",
535 SecurityTypeDetailed::Wpa3Enterprise => "WPA3Ent",
536 }
537 .to_string()
538 }
539
540 pub fn to_string_without_pii(&self) -> String {
541 let channel = self.bss.channel;
542 let rssi = self.bss.signal.rssi_dbm;
543 let recent_failure_count = self.recent_failure_count();
544 let recent_short_connection_count = self.recent_short_connections();
545
546 format!(
547 "{}({:4}), {}({:6}), {:>4}dBm, channel {:8}, score {:4}{}{}{}{}",
548 self.network.ssid,
549 self.saved_security_type_to_string(),
550 self.bss.bssid,
551 self.scanned_security_type_to_string(),
552 rssi,
553 channel,
554 scoring_functions::score_bss_scanned_candidate(self.clone()),
555 if !self.bss.is_compatible() { ", NOT compatible" } else { "" },
556 if recent_failure_count > 0 {
557 format!(", {recent_failure_count} recent failures")
558 } else {
559 "".to_string()
560 },
561 if recent_short_connection_count > 0 {
562 format!(", {recent_short_connection_count} recent short disconnects")
563 } else {
564 "".to_string()
565 },
566 if !self.saved_network_info.has_ever_connected { ", never used yet" } else { "" },
567 )
568 }
569}
570
571impl WriteInspect for types::ScannedCandidate {
572 fn write_inspect<'a>(&self, writer: &InspectNode, key: impl Into<Cow<'a, str>>) {
573 inspect_insert!(writer, var key: {
574 ssid: self.network.ssid.to_string(),
575 bssid: self.bss.bssid.to_string(),
576 rssi: self.bss.signal.rssi_dbm,
577 score: scoring_functions::score_bss_scanned_candidate(self.clone()),
578 security_type_saved: self.saved_security_type_to_string(),
579 security_type_scanned: format!(
580 "{}",
581 wlan_common::bss::Protection::from(self.security_type_detailed),
582 ),
583 channel: format!("{}", self.bss.channel),
584 compatible: self.bss.is_compatible(),
585 incompatibility: self.bss
586 .compatibility
587 .as_ref()
588 .err()
589 .map(ToString::to_string)
590 .unwrap_or_else(|| String::from("none")),
591 recent_failure_count: self.recent_failure_count(),
592 saved_network_has_ever_connected: self.saved_network_info.has_ever_connected,
593 });
594 }
595}
596
597fn get_authenticator(bss: &Bss, credential: &Credential) -> Option<SecurityAuthenticator> {
598 let mutual_security_protocols = match bss.compatibility.as_ref() {
599 Ok(compatible) => compatible.mutual_security_protocols().clone(),
600 Err(incompatible) => {
601 error!("BSS ({:?}) is incompatible: {}", bss.bssid, incompatible);
602 return None;
603 }
604 };
605
606 match select_authentication_method(mutual_security_protocols.clone(), credential) {
607 Some(authenticator) => Some(authenticator),
608 None => {
609 error!(
610 "Failed to negotiate authentication for BSS ({:?}) with mutually supported \
611 security protocols: {:?}, and credential type: {:?}.",
612 bss.bssid,
613 mutual_security_protocols,
614 credential.type_str()
615 );
616 None
617 }
618 }
619}
620
621fn merge_config_and_scan_data(
622 network_config: config_management::NetworkConfig,
623 scan_result: &mut types::ScanResult,
624) -> Vec<types::ScannedCandidate> {
625 if network_config.ssid != scan_result.ssid
626 || !network_config
627 .security_type
628 .is_compatible_with_scanned_type(&scan_result.security_type_detailed)
629 {
630 return Vec::new();
631 }
632
633 let mut merged_networks = Vec::new();
634 let multiple_bss_candidates = scan_result.entries.len() > 1;
635
636 for bss in scan_result.entries.iter() {
637 let authenticator = match get_authenticator(bss, &network_config.credential) {
638 Some(authenticator) => authenticator,
639 None => {
640 error!(
641 "Failed to create authenticator for bss candidate {:?} (SSID: {:?}). Removing from candidates.",
642 bss.bssid, network_config.ssid
643 );
644 continue;
645 }
646 };
647 let scanned_candidate = types::ScannedCandidate {
648 network: types::NetworkIdentifier {
649 ssid: network_config.ssid.clone(),
650 security_type: network_config.security_type,
651 },
652 security_type_detailed: scan_result.security_type_detailed,
653 credential: network_config.credential.clone(),
654 network_has_multiple_bss: multiple_bss_candidates,
655 saved_network_info: InternalSavedNetworkData {
656 has_ever_connected: network_config.has_ever_connected,
657 recent_failures: network_config.get_recent_connection_failures(
658 fasync::MonotonicInstant::now() - RECENT_FAILURE_WINDOW,
659 ),
660 past_connections: network_config.perf_stats.past_connections.clone(),
661 },
662 bss: bss.clone(),
663 authenticator,
664 };
665 merged_networks.push(scanned_candidate)
666 }
667 merged_networks
668}
669
670async fn merge_saved_networks_and_scan_data(
673 saved_network_manager: &Arc<dyn SavedNetworksManagerApi>,
674 mut scan_results: Vec<types::ScanResult>,
675) -> Vec<types::ScannedCandidate> {
676 let mut merged_networks = vec![];
677 for mut scan_result in scan_results.drain(..) {
678 for saved_config in saved_network_manager
679 .lookup_compatible(&scan_result.ssid, scan_result.security_type_detailed)
680 .await
681 {
682 let multiple_bss_candidates = scan_result.entries.len() > 1;
683 for bss in scan_result.entries.drain(..) {
684 let authenticator = match get_authenticator(&bss, &saved_config.credential) {
685 Some(authenticator) => authenticator,
686 None => {
687 error!(
688 "Failed to create authenticator for bss candidate {} (SSID: {}). Removing from candidates.",
689 bss.bssid, saved_config.ssid
690 );
691 continue;
692 }
693 };
694 let scanned_candidate = types::ScannedCandidate {
695 network: types::NetworkIdentifier {
696 ssid: saved_config.ssid.clone(),
697 security_type: saved_config.security_type,
698 },
699 security_type_detailed: scan_result.security_type_detailed,
700 credential: saved_config.credential.clone(),
701 network_has_multiple_bss: multiple_bss_candidates,
702 saved_network_info: InternalSavedNetworkData {
703 has_ever_connected: saved_config.has_ever_connected,
704 recent_failures: saved_config.get_recent_connection_failures(
705 fasync::MonotonicInstant::now() - RECENT_FAILURE_WINDOW,
706 ),
707 past_connections: saved_config.perf_stats.past_connections.clone(),
708 },
709 bss,
710 authenticator,
711 };
712 merged_networks.push(scanned_candidate)
713 }
714 }
715 }
716 merged_networks
717}
718
719fn record_metrics_on_scan(
720 mut merged_networks: Vec<types::ScannedCandidate>,
721 telemetry_sender: &TelemetrySender,
722) {
723 let mut merged_network_map: HashMap<types::NetworkIdentifier, Vec<types::ScannedCandidate>> =
724 HashMap::new();
725 for bss in merged_networks.drain(..) {
726 merged_network_map.entry(bss.network.clone()).or_default().push(bss);
727 }
728
729 let num_saved_networks_observed = merged_network_map.len();
730 let mut num_actively_scanned_networks = 0;
731 let bss_count_per_saved_network = merged_network_map
732 .values()
733 .map(|bsss| {
734 if bsss.iter().any(|bss| matches!(bss.bss.observation, types::ScanObservation::Active))
736 {
737 num_actively_scanned_networks += 1;
738 };
739 bsss.len()
741 })
742 .collect();
743
744 telemetry_sender.send(TelemetryEvent::ConnectionSelectionScanResults {
745 saved_network_count: num_saved_networks_observed,
746 bss_count_per_saved_network,
747 saved_network_count_found_by_active_scan: num_actively_scanned_networks,
748 });
749}
750#[cfg(test)]
751mod tests {
752 use super::*;
753 use crate::config_management::{ConnectFailure, FailureReason, SavedNetworksManager};
754 use crate::util::testing::fakes::{FakeSavedNetworksManager, FakeScanRequester};
755 use crate::util::testing::{
756 generate_channel, generate_random_bss, generate_random_connect_reason,
757 generate_random_network_identifier, generate_random_password, generate_random_scan_result,
758 generate_random_scanned_candidate,
759 };
760 use assert_matches::assert_matches;
761 use diagnostics_assertions::{AnyNumericProperty, assert_data_tree};
762 use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
763 use fidl_fuchsia_wlan_sme as fidl_sme;
764 use fuchsia_async as fasync;
765 use fuchsia_inspect as inspect;
766 use futures::task::Poll;
767 use ieee80211_testutils::BSSID_REGEX;
768 use std::pin::pin;
769 use std::rc::Rc;
770 use std::sync::LazyLock;
771 use test_case::test_case;
772 use wlan_common::bss::BssDescription;
773 use wlan_common::random_fidl_bss_description;
774 use wlan_common::scan::Compatible;
775 use wlan_common::security::SecurityDescriptor;
776
777 pub static TEST_PASSWORD: LazyLock<Credential> =
778 LazyLock::new(|| Credential::Password(b"password".to_vec()));
779
780 struct TestValues {
781 connection_selector: Rc<ConnectionSelector>,
782 real_saved_network_manager: Arc<dyn SavedNetworksManagerApi>,
783 saved_network_manager: Arc<FakeSavedNetworksManager>,
784 scan_requester: Arc<FakeScanRequester>,
785 inspector: inspect::Inspector,
786 telemetry_receiver: mpsc::Receiver<TelemetryEvent>,
787 }
788
789 async fn test_setup(use_real_save_network_manager: bool) -> TestValues {
790 let real_saved_network_manager = Arc::new(SavedNetworksManager::new_for_test().await);
791 let saved_network_manager = Arc::new(FakeSavedNetworksManager::new());
792 let scan_requester = Arc::new(FakeScanRequester::new());
793 let inspector = inspect::Inspector::default();
794 let inspect_node = inspector.root().create_child("connection_selection_test");
795 let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
796
797 let connection_selector = Rc::new(ConnectionSelector::new(
798 if use_real_save_network_manager {
799 real_saved_network_manager.clone()
800 } else {
801 saved_network_manager.clone()
802 },
803 scan_requester.clone(),
804 inspect_node,
805 TelemetrySender::new(telemetry_sender),
806 ));
807
808 TestValues {
809 connection_selector,
810 real_saved_network_manager,
811 saved_network_manager,
812 scan_requester,
813 inspector,
814 telemetry_receiver,
815 }
816 }
817
818 fn fake_successful_connect_result() -> fidl_sme::ConnectResult {
819 fidl_sme::ConnectResult {
820 code: fidl_ieee80211::StatusCode::Success,
821 is_credential_rejected: false,
822 is_reconnect: false,
823 }
824 }
825
826 #[fuchsia::test]
827 async fn scan_results_merged_with_saved_networks() {
828 let test_values = test_setup(true).await;
829
830 let test_ssid_1 = types::Ssid::try_from("foo").unwrap();
832 let test_security_1 = types::SecurityTypeDetailed::Wpa3Personal;
833 let test_id_1 = types::NetworkIdentifier {
834 ssid: test_ssid_1.clone(),
835 security_type: types::SecurityType::Wpa3,
836 };
837 let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
838 let test_ssid_2 = types::Ssid::try_from("bar").unwrap();
839 let test_security_2 = types::SecurityTypeDetailed::Wpa1;
840 let test_id_2 = types::NetworkIdentifier {
841 ssid: test_ssid_2.clone(),
842 security_type: types::SecurityType::Wpa,
843 };
844 let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec());
845
846 assert!(
848 test_values
849 .real_saved_network_manager
850 .store(test_id_1.clone(), credential_1.clone())
851 .await
852 .unwrap()
853 .is_none()
854 );
855
856 assert!(
857 test_values
858 .real_saved_network_manager
859 .store(test_id_2.clone(), credential_2.clone())
860 .await
861 .unwrap()
862 .is_none()
863 );
864
865 let mock_scan_results = vec![
867 types::ScanResult {
868 ssid: test_ssid_1.clone(),
869 security_type_detailed: test_security_1,
870 entries: vec![
871 types::Bss {
872 compatibility: Compatible::expect_ok([SecurityDescriptor::WPA3_PERSONAL]),
873 ..generate_random_bss()
874 },
875 types::Bss {
876 compatibility: Compatible::expect_ok([SecurityDescriptor::WPA3_PERSONAL]),
877 ..generate_random_bss()
878 },
879 types::Bss {
880 compatibility: Compatible::expect_ok([SecurityDescriptor::WPA3_PERSONAL]),
881 ..generate_random_bss()
882 },
883 ],
884 compatibility: types::Compatibility::Supported,
885 },
886 types::ScanResult {
887 ssid: test_ssid_2.clone(),
888 security_type_detailed: test_security_2,
889 entries: vec![types::Bss {
890 compatibility: Compatible::expect_ok([SecurityDescriptor::WPA1]),
891 ..generate_random_bss()
892 }],
893 compatibility: types::Compatibility::DisallowedNotSupported,
894 },
895 ];
896
897 let bssid_1 = mock_scan_results[0].entries[0].bssid;
898 let bssid_2 = mock_scan_results[0].entries[1].bssid;
899
900 test_values
902 .real_saved_network_manager
903 .record_connect_result(
904 test_id_1.clone(),
905 &credential_1.clone(),
906 bssid_1,
907 fake_successful_connect_result(),
908 types::ScanObservation::Unknown,
909 )
910 .await;
911
912 test_values
914 .real_saved_network_manager
915 .record_connect_result(
916 test_id_1.clone(),
917 &credential_1.clone(),
918 bssid_2,
919 fidl_sme::ConnectResult {
920 code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
921 is_credential_rejected: true,
922 ..fake_successful_connect_result()
923 },
924 types::ScanObservation::Unknown,
925 )
926 .await;
927
928 let saved_config = test_values
930 .real_saved_network_manager
931 .lookup(&test_id_1.clone())
932 .await
933 .expect("failed to get config");
934 let failure_time = saved_config
935 .get_recent_connection_failures(fasync::MonotonicInstant::now() - RECENT_FAILURE_WINDOW)
936 .first()
937 .expect("failed to get recent failure")
938 .time;
939 let recent_failures = vec![ConnectFailure {
940 bssid: bssid_2,
941 time: failure_time,
942 reason: FailureReason::CredentialRejected,
943 }];
944 let expected_internal_data_1 = InternalSavedNetworkData {
945 has_ever_connected: true,
946 recent_failures: recent_failures.clone(),
947 past_connections: HashMap::new(),
948 };
949 let wpa3_authenticator = select_authentication_method(
950 HashSet::from([SecurityDescriptor::WPA3_PERSONAL]),
951 &credential_1,
952 )
953 .unwrap();
954 let open_authenticator =
955 select_authentication_method(HashSet::from([SecurityDescriptor::WPA1]), &credential_2)
956 .unwrap();
957 let expected_results = vec![
958 types::ScannedCandidate {
959 network: test_id_1.clone(),
960 credential: credential_1.clone(),
961 network_has_multiple_bss: true,
962 security_type_detailed: test_security_1,
963 saved_network_info: expected_internal_data_1.clone(),
964 bss: mock_scan_results[0].entries[0].clone(),
965 authenticator: wpa3_authenticator.clone(),
966 },
967 types::ScannedCandidate {
968 network: test_id_1.clone(),
969 credential: credential_1.clone(),
970 network_has_multiple_bss: true,
971 security_type_detailed: test_security_1,
972 saved_network_info: expected_internal_data_1.clone(),
973 bss: mock_scan_results[0].entries[1].clone(),
974 authenticator: wpa3_authenticator.clone(),
975 },
976 types::ScannedCandidate {
977 network: test_id_1.clone(),
978 credential: credential_1.clone(),
979 network_has_multiple_bss: true,
980 security_type_detailed: test_security_1,
981 saved_network_info: expected_internal_data_1.clone(),
982 bss: mock_scan_results[0].entries[2].clone(),
983 authenticator: wpa3_authenticator.clone(),
984 },
985 types::ScannedCandidate {
986 network: test_id_2.clone(),
987 credential: credential_2.clone(),
988 network_has_multiple_bss: false,
989 security_type_detailed: test_security_2,
990 saved_network_info: InternalSavedNetworkData {
991 has_ever_connected: false,
992 recent_failures: Vec::new(),
993 past_connections: HashMap::new(),
994 },
995 bss: mock_scan_results[1].entries[0].clone(),
996 authenticator: open_authenticator.clone(),
997 },
998 ];
999
1000 let results = merge_saved_networks_and_scan_data(
1002 #[allow(clippy::arc_with_non_send_sync)]
1004 &Arc::new(test_values.real_saved_network_manager),
1005 mock_scan_results,
1006 )
1007 .await;
1008
1009 assert_eq!(results, expected_results);
1010 }
1011
1012 #[fuchsia::test]
1013 fn augment_bss_candidate_with_active_scan_doesnt_run_on_actively_found_networks() {
1014 let mut exec = fasync::TestExecutor::new();
1015 let test_values = exec.run_singlethreaded(test_setup(true));
1016 let mut candidate = generate_random_scanned_candidate();
1017 candidate.bss.observation = types::ScanObservation::Active;
1018
1019 let fut = test_values
1020 .connection_selector
1021 .augment_bss_candidate_with_active_scan(candidate.clone());
1022 let mut fut = pin!(fut);
1023
1024 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(res) => {
1026 assert_eq!(&res, &candidate);
1027 });
1028 }
1029
1030 #[fuchsia::test]
1031 fn augment_bss_candidate_with_active_scan_runs_on_passively_found_networks() {
1032 let mut exec = fasync::TestExecutor::new();
1033 let test_values = exec.run_singlethreaded(test_setup(true));
1034
1035 let mut passively_scanned_candidate = generate_random_scanned_candidate();
1036 passively_scanned_candidate.bss.observation = types::ScanObservation::Passive;
1037
1038 let fut = test_values
1039 .connection_selector
1040 .augment_bss_candidate_with_active_scan(passively_scanned_candidate.clone());
1041 let fut = pin!(fut);
1042
1043 let new_bss_desc = random_fidl_bss_description!();
1045 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1046 types::ScanResult {
1047 ssid: passively_scanned_candidate.network.ssid.clone(),
1048 security_type_detailed: passively_scanned_candidate.security_type_detailed,
1049 compatibility: types::Compatibility::Supported,
1050 entries: vec![types::Bss {
1051 bssid: passively_scanned_candidate.bss.bssid,
1052 compatibility: wlan_common::scan::Compatible::expect_ok([
1053 wlan_common::security::SecurityDescriptor::WPA1,
1054 ]),
1055 bss_description: new_bss_desc.clone().into(),
1056 ..generate_random_bss()
1057 }],
1058 },
1059 ])));
1060
1061 let candidate = exec.run_singlethreaded(fut);
1062 assert_eq!(
1064 &candidate,
1065 &types::ScannedCandidate {
1066 bss: types::Bss {
1067 bss_description: new_bss_desc.into(),
1068 ..passively_scanned_candidate.bss.clone()
1069 },
1070 ..passively_scanned_candidate.clone()
1071 },
1072 );
1073
1074 assert_eq!(
1076 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1077 vec![(
1078 ScanReason::BssSelectionAugmentation,
1079 vec![passively_scanned_candidate.network.ssid.clone()],
1080 vec![passively_scanned_candidate.bss.channel]
1081 )]
1082 );
1083 }
1084
1085 #[fuchsia::test]
1086 fn find_available_bss_list_with_network_specified() {
1087 let mut exec = fasync::TestExecutor::new();
1088 let test_values = exec.run_singlethreaded(test_setup(false));
1089 let connection_selector = test_values.connection_selector;
1090
1091 let test_id_1 = types::NetworkIdentifier {
1093 ssid: types::Ssid::try_from("foo").unwrap(),
1094 security_type: types::SecurityType::Wpa3,
1095 };
1096 let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
1097
1098 assert!(
1100 exec.run_singlethreaded(
1101 test_values.saved_network_manager.store(test_id_1.clone(), credential_1.clone()),
1102 )
1103 .unwrap()
1104 .is_none()
1105 );
1106
1107 let mutual_security_protocols_1 = [SecurityDescriptor::WPA3_PERSONAL];
1109 let bss_desc_1 = random_fidl_bss_description!();
1110 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1111 types::ScanResult {
1112 ssid: test_id_1.ssid.clone(),
1113 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1114 compatibility: types::Compatibility::Supported,
1115 entries: vec![types::Bss {
1116 compatibility: wlan_common::scan::Compatible::expect_ok(
1117 mutual_security_protocols_1,
1118 ),
1119 bss_description: bss_desc_1.clone().into(),
1120 ..generate_random_bss()
1121 }],
1122 },
1123 generate_random_scan_result(),
1124 generate_random_scan_result(),
1125 ])));
1126
1127 let fut = connection_selector.find_available_bss_candidate_list(Some(test_id_1.clone()));
1129 let mut fut = pin!(fut);
1130 let results = exec.run_singlethreaded(&mut fut);
1131 assert_eq!(results.len(), 1);
1132
1133 assert_eq!(
1135 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1136 vec![(ScanReason::BssSelection, vec![test_id_1.ssid.clone()], vec![])]
1137 );
1138 }
1139
1140 #[test_case(true)]
1141 #[test_case(false)]
1142 #[fuchsia::test(add_test_attr = false)]
1143 fn find_available_bss_list_without_network_specified(hidden: bool) {
1144 let mut exec = fasync::TestExecutor::new();
1145 let mut test_values = exec.run_singlethreaded(test_setup(false));
1146 let connection_selector = test_values.connection_selector;
1147
1148 let test_id_not_hidden = types::NetworkIdentifier {
1150 ssid: types::Ssid::try_from("foo").unwrap(),
1151 security_type: types::SecurityType::Wpa3,
1152 };
1153 let test_id_maybe_hidden = types::NetworkIdentifier {
1154 ssid: types::Ssid::try_from("bar").unwrap(),
1155 security_type: types::SecurityType::Wpa3,
1156 };
1157 let test_id_hidden_but_seen = types::NetworkIdentifier {
1158 ssid: types::Ssid::try_from("baz").unwrap(),
1159 security_type: types::SecurityType::Wpa3,
1160 };
1161 let credential = Credential::Password("some_pass".as_bytes().to_vec());
1162
1163 assert!(
1165 exec.run_singlethreaded(
1166 test_values
1167 .saved_network_manager
1168 .store(test_id_not_hidden.clone(), credential.clone()),
1169 )
1170 .unwrap()
1171 .is_none()
1172 );
1173 assert!(
1174 exec.run_singlethreaded(
1175 test_values
1176 .saved_network_manager
1177 .store(test_id_maybe_hidden.clone(), credential.clone()),
1178 )
1179 .unwrap()
1180 .is_none()
1181 );
1182 assert!(
1183 exec.run_singlethreaded(
1184 test_values
1185 .saved_network_manager
1186 .store(test_id_hidden_but_seen.clone(), credential.clone()),
1187 )
1188 .unwrap()
1189 .is_none()
1190 );
1191
1192 exec.run_singlethreaded(
1194 test_values.saved_network_manager.update_hidden_prob(test_id_not_hidden.clone(), 0.0),
1195 );
1196 exec.run_singlethreaded(
1197 test_values
1198 .saved_network_manager
1199 .update_hidden_prob(test_id_hidden_but_seen.clone(), 1.0),
1200 );
1201 exec.run_singlethreaded(
1203 test_values
1204 .saved_network_manager
1205 .update_hidden_prob(test_id_maybe_hidden.clone(), if hidden { 1.0 } else { 0.0 }),
1206 );
1207
1208 let mutual_security_protocols = [SecurityDescriptor::WPA3_PERSONAL];
1210 let bss_desc = random_fidl_bss_description!();
1211 if hidden {
1212 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1214 types::ScanResult {
1215 ssid: test_id_not_hidden.ssid.clone(),
1216 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1217 compatibility: types::Compatibility::Supported,
1218 entries: vec![types::Bss {
1219 compatibility: wlan_common::scan::Compatible::expect_ok(
1220 mutual_security_protocols,
1221 ),
1222 bss_description: bss_desc.clone().into(),
1223 ..generate_random_bss()
1224 }],
1225 },
1226 types::ScanResult {
1227 ssid: test_id_hidden_but_seen.ssid.clone(),
1228 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1229 compatibility: types::Compatibility::Supported,
1230 entries: vec![types::Bss {
1231 compatibility: wlan_common::scan::Compatible::expect_ok(
1232 mutual_security_protocols,
1233 ),
1234 bss_description: bss_desc.clone().into(),
1235 ..generate_random_bss()
1236 }],
1237 },
1238 generate_random_scan_result(),
1239 generate_random_scan_result(),
1240 ])));
1241 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1243 types::ScanResult {
1244 ssid: test_id_maybe_hidden.ssid.clone(),
1245 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1246 compatibility: types::Compatibility::Supported,
1247 entries: vec![types::Bss {
1248 compatibility: wlan_common::scan::Compatible::expect_ok(
1249 mutual_security_protocols,
1250 ),
1251 bss_description: bss_desc.clone().into(),
1252 ..generate_random_bss()
1253 }],
1254 },
1255 generate_random_scan_result(),
1256 generate_random_scan_result(),
1257 ])));
1258 } else {
1259 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1261 types::ScanResult {
1262 ssid: test_id_not_hidden.ssid.clone(),
1263 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1264 compatibility: types::Compatibility::Supported,
1265 entries: vec![types::Bss {
1266 compatibility: wlan_common::scan::Compatible::expect_ok(
1267 mutual_security_protocols,
1268 ),
1269 bss_description: bss_desc.clone().into(),
1270 ..generate_random_bss()
1271 }],
1272 },
1273 types::ScanResult {
1274 ssid: test_id_maybe_hidden.ssid.clone(),
1275 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1276 compatibility: types::Compatibility::Supported,
1277 entries: vec![types::Bss {
1278 compatibility: wlan_common::scan::Compatible::expect_ok(
1279 mutual_security_protocols,
1280 ),
1281 bss_description: bss_desc.clone().into(),
1282 ..generate_random_bss()
1283 }],
1284 },
1285 types::ScanResult {
1286 ssid: test_id_hidden_but_seen.ssid.clone(),
1287 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1288 compatibility: types::Compatibility::Supported,
1289 entries: vec![types::Bss {
1290 compatibility: wlan_common::scan::Compatible::expect_ok(
1291 mutual_security_protocols,
1292 ),
1293 bss_description: bss_desc.clone().into(),
1294 ..generate_random_bss()
1295 }],
1296 },
1297 generate_random_scan_result(),
1298 generate_random_scan_result(),
1299 ])));
1300 }
1301
1302 let connection_selection_fut = connection_selector.find_available_bss_candidate_list(None);
1304 let mut connection_selection_fut = pin!(connection_selection_fut);
1305 let results = exec.run_singlethreaded(&mut connection_selection_fut);
1306 assert_eq!(results.len(), 3);
1307
1308 if hidden {
1311 assert_eq!(
1312 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1313 vec![
1314 (ScanReason::NetworkSelection, vec![], vec![]),
1315 (ScanReason::NetworkSelection, vec![test_id_maybe_hidden.ssid.clone()], vec![])
1316 ]
1317 )
1318 } else {
1319 assert_eq!(
1320 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1321 vec![(ScanReason::NetworkSelection, vec![], vec![])]
1322 )
1323 }
1324
1325 assert_matches!(
1327 test_values.telemetry_receiver.try_recv(),
1328 Ok(TelemetryEvent::ActiveScanRequested{num_ssids_requested}) => {
1329 if hidden {
1330 assert_eq!(num_ssids_requested, 1);
1331 } else {
1332 assert_eq!(num_ssids_requested, 0);
1333 }
1334 });
1335 }
1336
1337 #[fuchsia::test]
1338 fn find_and_select_connection_candidate_scan_error() {
1339 let mut exec = fasync::TestExecutor::new();
1340 let test_values = exec.run_singlethreaded(test_setup(true));
1341 let connection_selector = test_values.connection_selector;
1342 let mut telemetry_receiver = test_values.telemetry_receiver;
1343
1344 exec.run_singlethreaded(
1346 test_values.scan_requester.add_scan_result(Err(types::ScanError::GeneralError)),
1347 );
1348
1349 let mut connection_selection_fut = pin!(
1351 connection_selector
1352 .find_and_select_connection_candidate(None, generate_random_connect_reason())
1353 );
1354 assert_matches!(exec.run_until_stalled(&mut connection_selection_fut), Poll::Ready(None));
1356
1357 assert_eq!(
1359 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1360 vec![(scan::ScanReason::NetworkSelection, vec![], vec![])]
1361 );
1362
1363 assert_data_tree!(@executor exec, test_values.inspector, root: {
1365 connection_selection_test: {
1366 connection_selection: {
1367 "0": {
1368 "@time": AnyNumericProperty,
1369 "candidates": {},
1370 },
1371 }
1372 },
1373 });
1374
1375 assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
1377 assert_matches!(event, TelemetryEvent::NetworkSelectionDecision {
1378 network_selection_type: telemetry::NetworkSelectionType::Undirected,
1379 num_candidates: Err(()),
1380 selected_count: 0,
1381 });
1382 });
1383 assert_matches!(
1384 telemetry_receiver.try_recv(),
1385 Ok(TelemetryEvent::BssSelectionResult { selected_candidate: None, .. })
1386 );
1387 }
1388
1389 #[fuchsia::test]
1390 fn find_and_select_connection_candidate_end_to_end() {
1391 let mut exec = fasync::TestExecutor::new();
1392 let test_values = exec.run_singlethreaded(test_setup(true));
1393 let connection_selector = test_values.connection_selector;
1394 let mut telemetry_receiver = test_values.telemetry_receiver;
1395
1396 let test_id_1 = types::NetworkIdentifier {
1398 ssid: types::Ssid::try_from("foo").unwrap(),
1399 security_type: types::SecurityType::Wpa3,
1400 };
1401 let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
1402 let bssid_1 = types::Bssid::from([1, 1, 1, 1, 1, 1]);
1403
1404 let test_id_2 = types::NetworkIdentifier {
1405 ssid: types::Ssid::try_from("bar").unwrap(),
1406 security_type: types::SecurityType::Wpa,
1407 };
1408 let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec());
1409 let bssid_2 = types::Bssid::from([2, 2, 2, 2, 2, 2]);
1410
1411 assert!(
1413 exec.run_singlethreaded(
1414 test_values
1415 .real_saved_network_manager
1416 .store(test_id_1.clone(), credential_1.clone()),
1417 )
1418 .unwrap()
1419 .is_none()
1420 );
1421 assert!(
1422 exec.run_singlethreaded(
1423 test_values
1424 .real_saved_network_manager
1425 .store(test_id_2.clone(), credential_2.clone()),
1426 )
1427 .unwrap()
1428 .is_none()
1429 );
1430
1431 exec.run_singlethreaded(test_values.real_saved_network_manager.record_connect_result(
1433 test_id_1.clone(),
1434 &credential_1.clone(),
1435 bssid_1,
1436 fake_successful_connect_result(),
1437 types::ScanObservation::Passive,
1438 ));
1439 exec.run_singlethreaded(test_values.real_saved_network_manager.record_connect_result(
1440 test_id_2.clone(),
1441 &credential_2.clone(),
1442 bssid_2,
1443 fake_successful_connect_result(),
1444 types::ScanObservation::Passive,
1445 ));
1446
1447 let mutual_security_protocols_1 = [SecurityDescriptor::WPA3_PERSONAL];
1449 let channel_1 = generate_channel(3, fidl_fuchsia_wlan_ieee80211::WlanBand::TwoGhz);
1450 let mutual_security_protocols_2 = [SecurityDescriptor::WPA1];
1451 let channel_2 = generate_channel(50, fidl_fuchsia_wlan_ieee80211::WlanBand::FiveGhz);
1452 let mock_passive_scan_results = vec![
1453 types::ScanResult {
1454 ssid: test_id_1.ssid.clone(),
1455 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1456 compatibility: types::Compatibility::Supported,
1457 entries: vec![types::Bss {
1458 compatibility: wlan_common::scan::Compatible::expect_ok(
1459 mutual_security_protocols_1,
1460 ),
1461 bssid: bssid_1,
1462 channel: channel_1,
1463 signal: types::Signal {
1464 rssi_dbm: -30, snr_db: 0,
1466 },
1467 observation: types::ScanObservation::Passive,
1468 ..generate_random_bss()
1469 }],
1470 },
1471 types::ScanResult {
1472 ssid: test_id_2.ssid.clone(),
1473 security_type_detailed: types::SecurityTypeDetailed::Wpa1,
1474 compatibility: types::Compatibility::Supported,
1475 entries: vec![types::Bss {
1476 compatibility: wlan_common::scan::Compatible::expect_ok(
1477 mutual_security_protocols_2,
1478 ),
1479 bssid: bssid_2,
1480 channel: channel_2,
1481 signal: types::Signal {
1482 rssi_dbm: -100, snr_db: 0,
1484 },
1485 observation: types::ScanObservation::Passive,
1486 ..generate_random_bss()
1487 }],
1488 },
1489 generate_random_scan_result(),
1490 generate_random_scan_result(),
1491 ];
1492
1493 exec.run_singlethreaded(
1495 test_values.scan_requester.add_scan_result(Ok(mock_passive_scan_results.clone())),
1496 );
1497
1498 let bss_desc1_active = random_fidl_bss_description!();
1500 let new_bss = types::Bss {
1501 compatibility: wlan_common::scan::Compatible::expect_ok(mutual_security_protocols_1),
1502 bssid: bssid_1,
1503 bss_description: bss_desc1_active.clone().into(),
1504 ..generate_random_bss()
1505 };
1506 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1507 types::ScanResult {
1508 ssid: test_id_1.ssid.clone(),
1509 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1510 compatibility: types::Compatibility::Supported,
1511 entries: vec![new_bss.clone()],
1512 },
1513 generate_random_scan_result(),
1514 ])));
1515
1516 let mut connection_selection_fut = pin!(
1518 connection_selector
1519 .find_and_select_connection_candidate(None, generate_random_connect_reason())
1520 );
1521 let results =
1522 exec.run_singlethreaded(&mut connection_selection_fut).expect("no selected candidate");
1523 assert_eq!(&results.network, &test_id_1.clone());
1524 assert_eq!(
1525 &results.bss,
1526 &types::Bss {
1527 bss_description: bss_desc1_active.clone().into(),
1528 ..mock_passive_scan_results[0].entries[0].clone()
1529 }
1530 );
1531
1532 assert_eq!(
1534 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1535 vec![
1536 (ScanReason::NetworkSelection, vec![], vec![]),
1538 (
1540 ScanReason::BssSelectionAugmentation,
1541 vec![test_id_1.ssid.clone()],
1542 vec![channel_1]
1543 )
1544 ]
1545 );
1546
1547 assert_data_tree!(@executor exec, test_values.inspector, root: {
1549 connection_selection_test: {
1550 connection_selection: {
1551 "0": {
1552 "@time": AnyNumericProperty,
1553 "candidates": {
1554 "0": contains {
1555 bssid: &*BSSID_REGEX,
1556 score: AnyNumericProperty,
1557 },
1558 "1": contains {
1559 bssid: &*BSSID_REGEX,
1560 score: AnyNumericProperty,
1561 },
1562 },
1563 "selected": contains {
1564 bssid: &*BSSID_REGEX,
1565 score: AnyNumericProperty,
1566 },
1567 },
1568 }
1569 },
1570 });
1571
1572 assert_matches!(
1574 telemetry_receiver.try_recv(),
1575 Ok(TelemetryEvent::ActiveScanRequested { num_ssids_requested: 0 })
1576 );
1577 assert_matches!(
1578 telemetry_receiver.try_recv(), Ok(TelemetryEvent::ConnectionSelectionScanResults {
1579 saved_network_count, bss_count_per_saved_network, saved_network_count_found_by_active_scan
1580 }) => {
1581 assert_eq!(saved_network_count, 2);
1582 assert_eq!(bss_count_per_saved_network, vec![1, 1]);
1583 assert_eq!(saved_network_count_found_by_active_scan, 0);
1584 }
1585 );
1586 assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
1587 assert_matches!(event, TelemetryEvent::NetworkSelectionDecision {
1588 network_selection_type: telemetry::NetworkSelectionType::Undirected,
1589 num_candidates: Ok(2),
1590 selected_count: 2,
1591 });
1592 });
1593 assert_matches!(
1594 telemetry_receiver.try_recv(),
1595 Ok(TelemetryEvent::BssSelectionResult { .. })
1596 );
1597 }
1598
1599 #[fuchsia::test]
1600 fn find_and_select_connection_candidate_with_network_end_to_end() {
1601 let mut exec = fasync::TestExecutor::new();
1602 let test_values = exec.run_singlethreaded(test_setup(true));
1603 let connection_selector = test_values.connection_selector;
1604 let mut telemetry_receiver = test_values.telemetry_receiver;
1605
1606 let test_id_1 = types::NetworkIdentifier {
1608 ssid: types::Ssid::try_from("foo").unwrap(),
1609 security_type: types::SecurityType::Wpa3,
1610 };
1611
1612 assert!(
1614 exec.run_singlethreaded(
1615 test_values
1616 .real_saved_network_manager
1617 .store(test_id_1.clone(), TEST_PASSWORD.clone()),
1618 )
1619 .unwrap()
1620 .is_none()
1621 );
1622
1623 let mutual_security_protocols_1 = [SecurityDescriptor::WPA3_PERSONAL];
1625 let bss_desc_1 = random_fidl_bss_description!();
1626 let scanned_bss = types::Bss {
1627 compatibility: wlan_common::scan::Compatible::expect_ok(mutual_security_protocols_1),
1629 bss_description: bss_desc_1.clone().into(),
1630 observation: types::ScanObservation::Unknown,
1632 ..generate_random_bss()
1633 };
1634 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1635 types::ScanResult {
1636 ssid: test_id_1.ssid.clone(),
1637 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1639 compatibility: types::Compatibility::Supported,
1640 entries: vec![scanned_bss.clone()],
1641 },
1642 generate_random_scan_result(),
1643 generate_random_scan_result(),
1644 ])));
1645
1646 let connection_selection_fut = connection_selector.find_and_select_connection_candidate(
1648 Some(test_id_1.clone()),
1649 generate_random_connect_reason(),
1650 );
1651 let mut connection_selection_fut = pin!(connection_selection_fut);
1652 let results =
1653 exec.run_singlethreaded(&mut connection_selection_fut).expect("no selected candidate");
1654 assert_eq!(&results.network, &test_id_1);
1655 assert_eq!(&results.security_type_detailed, &types::SecurityTypeDetailed::Wpa3Personal);
1656 assert_eq!(&results.credential, &TEST_PASSWORD.clone());
1657 assert_eq!(&results.bss, &scanned_bss);
1658
1659 assert_eq!(
1661 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1662 vec![(ScanReason::BssSelection, vec![test_id_1.ssid.clone()], vec![])]
1663 );
1664 assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
1666 assert_matches!(event, TelemetryEvent::NetworkSelectionDecision {
1667 network_selection_type: telemetry::NetworkSelectionType::Directed,
1668 num_candidates: Ok(1),
1669 selected_count: 1,
1670 });
1671 });
1672 assert_matches!(
1673 telemetry_receiver.try_recv(),
1674 Ok(TelemetryEvent::BssSelectionResult { .. })
1675 );
1676 }
1677
1678 #[fuchsia::test]
1679 fn find_and_select_connection_candidate_with_network_end_to_end_with_failure() {
1680 let mut exec = fasync::TestExecutor::new();
1681 let test_values = exec.run_singlethreaded(test_setup(true));
1682 let connection_selector = test_values.connection_selector;
1683 let mut telemetry_receiver = test_values.telemetry_receiver;
1684
1685 let test_id_1 = types::NetworkIdentifier {
1687 ssid: types::Ssid::try_from("foo").unwrap(),
1688 security_type: types::SecurityType::Wpa3,
1689 };
1690
1691 exec.run_singlethreaded(
1693 test_values.scan_requester.add_scan_result(Err(types::ScanError::GeneralError)),
1694 );
1695
1696 let connection_selection_fut = connection_selector.find_and_select_connection_candidate(
1698 Some(test_id_1.clone()),
1699 generate_random_connect_reason(),
1700 );
1701 let mut connection_selection_fut = pin!(connection_selection_fut);
1702
1703 let results = exec.run_singlethreaded(&mut connection_selection_fut);
1705 assert_eq!(results, None);
1706
1707 assert_eq!(
1709 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1710 vec![(ScanReason::BssSelection, vec![test_id_1.ssid.clone()], vec![])]
1711 );
1712
1713 assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
1715 assert_matches!(event, TelemetryEvent::NetworkSelectionDecision {
1716 network_selection_type: telemetry::NetworkSelectionType::Directed,
1717 num_candidates: Err(()),
1718 selected_count: 1,
1719 });
1720 });
1721
1722 assert_matches!(
1723 telemetry_receiver.try_recv(),
1724 Ok(TelemetryEvent::BssSelectionResult { .. })
1725 );
1726 }
1727
1728 #[test_case(fidl_common::ScanType::Active)]
1729 #[test_case(fidl_common::ScanType::Passive)]
1730 #[fuchsia::test(add_test_attr = false)]
1731 fn find_and_select_roam_candidate_requests_typed_scan(scan_type: fidl_common::ScanType) {
1732 let mut exec = fasync::TestExecutor::new();
1733 let test_values = exec.run_singlethreaded(test_setup(true));
1734 let connection_selector = test_values.connection_selector;
1735
1736 let test_id = types::NetworkIdentifier {
1737 ssid: types::Ssid::try_from("foo").unwrap(),
1738 security_type: types::SecurityType::Wpa3,
1739 };
1740 let credential = generate_random_password();
1741
1742 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1744 types::ScanResult {
1745 ssid: test_id.ssid.clone(),
1746 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1747 compatibility: types::Compatibility::Supported,
1748 entries: vec![],
1749 },
1750 ])));
1751
1752 let roam_selection_fut = connection_selector.find_and_select_roam_candidate(
1754 scan_type,
1755 test_id.clone(),
1756 &credential,
1757 types::SecurityTypeDetailed::Wpa3Personal,
1758 );
1759 let mut roam_selection_fut = pin!(roam_selection_fut);
1760 let _ = exec.run_singlethreaded(&mut roam_selection_fut);
1761
1762 if scan_type == fidl_common::ScanType::Active {
1764 assert_eq!(
1765 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1766 vec![(ScanReason::RoamSearch, vec![test_id.ssid.clone()], vec![])]
1767 );
1768 } else {
1769 assert_eq!(
1770 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1771 vec![(ScanReason::RoamSearch, vec![], vec![])]
1772 );
1773 }
1774 }
1775
1776 #[fuchsia::test]
1777 fn find_and_select_roam_candidate_active_scans_if_no_results_found() {
1778 let mut exec = fasync::TestExecutor::new();
1779 let test_values = exec.run_singlethreaded(test_setup(true));
1780 let connection_selector = test_values.connection_selector;
1781
1782 let test_id = generate_random_network_identifier();
1783 let credential = generate_random_password();
1784
1785 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![])));
1787 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![])));
1788
1789 let roam_selection_fut = connection_selector.find_and_select_roam_candidate(
1791 fidl_common::ScanType::Passive,
1792 test_id.clone(),
1793 &credential,
1794 types::SecurityTypeDetailed::Open,
1795 );
1796
1797 let mut roam_selection_fut = pin!(roam_selection_fut);
1798 let _ = exec.run_singlethreaded(&mut roam_selection_fut);
1799
1800 assert_eq!(
1803 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1804 vec![
1805 (ScanReason::RoamSearch, vec![], vec![]),
1806 (ScanReason::RoamSearch, vec![test_id.ssid.clone()], vec![])
1807 ]
1808 );
1809 }
1810
1811 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
1812 #[fuchsia::test]
1813 async fn recorded_metrics_on_scan() {
1814 let (telemetry_sender, mut telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1815 let telemetry_sender = TelemetrySender::new(telemetry_sender);
1816
1817 let test_id_1 = types::NetworkIdentifier {
1819 ssid: types::Ssid::try_from("foo").unwrap().clone(),
1820 security_type: types::SecurityType::Wpa3,
1821 };
1822
1823 let test_id_2 = types::NetworkIdentifier {
1824 ssid: types::Ssid::try_from("bar").unwrap().clone(),
1825 security_type: types::SecurityType::Wpa,
1826 };
1827
1828 let mut mock_scan_results = vec![];
1829
1830 mock_scan_results.push(types::ScannedCandidate {
1831 network: test_id_1.clone(),
1832 bss: types::Bss {
1833 observation: types::ScanObservation::Passive,
1834 ..generate_random_bss()
1835 },
1836 ..generate_random_scanned_candidate()
1837 });
1838 mock_scan_results.push(types::ScannedCandidate {
1839 network: test_id_1.clone(),
1840 bss: types::Bss {
1841 observation: types::ScanObservation::Passive,
1842 ..generate_random_bss()
1843 },
1844 ..generate_random_scanned_candidate()
1845 });
1846 mock_scan_results.push(types::ScannedCandidate {
1847 network: test_id_1.clone(),
1848 bss: types::Bss {
1849 observation: types::ScanObservation::Active,
1850 ..generate_random_bss()
1851 },
1852 ..generate_random_scanned_candidate()
1853 });
1854 mock_scan_results.push(types::ScannedCandidate {
1855 network: test_id_2.clone(),
1856 bss: types::Bss {
1857 observation: types::ScanObservation::Passive,
1858 ..generate_random_bss()
1859 },
1860 ..generate_random_scanned_candidate()
1861 });
1862
1863 record_metrics_on_scan(mock_scan_results, &telemetry_sender);
1864
1865 assert_matches!(
1866 telemetry_receiver.try_recv(), Ok(TelemetryEvent::ConnectionSelectionScanResults {
1867 saved_network_count, mut bss_count_per_saved_network, saved_network_count_found_by_active_scan
1868 }) => {
1869 assert_eq!(saved_network_count, 2);
1870 bss_count_per_saved_network.sort();
1871 assert_eq!(bss_count_per_saved_network, vec![1, 3]);
1872 assert_eq!(saved_network_count_found_by_active_scan, 1);
1873 }
1874 );
1875 }
1876
1877 #[fuchsia::test]
1878 async fn recorded_metrics_on_scan_no_saved_networks() {
1879 let (telemetry_sender, mut telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1880 let telemetry_sender = TelemetrySender::new(telemetry_sender);
1881
1882 let mock_scan_results = vec![];
1883
1884 record_metrics_on_scan(mock_scan_results, &telemetry_sender);
1885
1886 assert_matches!(
1887 telemetry_receiver.try_recv(), Ok(TelemetryEvent::ConnectionSelectionScanResults {
1888 saved_network_count, bss_count_per_saved_network, saved_network_count_found_by_active_scan
1889 }) => {
1890 assert_eq!(saved_network_count, 0);
1891 assert_eq!(bss_count_per_saved_network, Vec::<usize>::new());
1892 assert_eq!(saved_network_count_found_by_active_scan, 0);
1893 }
1894 );
1895 }
1896
1897 struct FakeConnectionSelector {
1900 pub response_to_find_and_select_connection_candidate: Option<types::ScannedCandidate>,
1901 pub response_to_find_and_select_roam_candidate: Option<types::ScannedCandidate>,
1902 }
1903 #[async_trait(?Send)]
1904 impl ConnectionSelectorApi for FakeConnectionSelector {
1905 async fn find_and_select_connection_candidate(
1906 &self,
1907 _network: Option<types::NetworkIdentifier>,
1908 _reason: types::ConnectReason,
1909 ) -> Option<types::ScannedCandidate> {
1910 self.response_to_find_and_select_connection_candidate.clone()
1911 }
1912 async fn find_and_select_roam_candidate(
1913 &self,
1914 _scan_type: fidl_common::ScanType,
1915 _network: types::NetworkIdentifier,
1916 _credential: &network_config::Credential,
1917 _scanned_securioty_type: types::SecurityTypeDetailed,
1918 ) -> Option<types::ScannedCandidate> {
1919 self.response_to_find_and_select_roam_candidate.clone()
1920 }
1921 }
1922 #[fuchsia::test]
1923 fn test_service_loop_passes_connection_selection_request() {
1924 let mut exec = fasync::TestExecutor::new();
1925
1926 let candidate = generate_random_scanned_candidate();
1928 let connection_selector = Rc::new(FakeConnectionSelector {
1929 response_to_find_and_select_connection_candidate: Some(candidate.clone()),
1930 response_to_find_and_select_roam_candidate: None,
1931 });
1932
1933 let (request_sender, request_receiver) = mpsc::channel(5);
1935 let power_manager = Arc::new(wlan_power_manager_testing::TestPowerManager::new());
1936 let mut serve_fut = pin!(serve_connection_selection_request_loop(
1937 connection_selector,
1938 request_receiver,
1939 power_manager.clone(),
1940 ));
1941 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
1942
1943 let mut requester = ConnectionSelectionRequester { sender: request_sender };
1945
1946 let mut connection_selection_fut = pin!(
1948 requester.do_connection_selection(None, types::ConnectReason::IdleInterfaceAutoconnect)
1949 );
1950 assert_matches!(exec.run_until_stalled(&mut connection_selection_fut), Poll::Pending);
1951
1952 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
1954
1955 assert_matches!(exec.run_until_stalled(&mut connection_selection_fut), Poll::Ready(Ok(Some(selected_candidate))) => {
1957 assert_eq!(selected_candidate, candidate);
1958 });
1959
1960 let calls = power_manager.calls.lock();
1961 assert!(calls.contains(&"wlancfg-connection-selection".to_string()));
1962 }
1963
1964 #[fuchsia::test]
1965 fn test_service_loop_passes_roam_selection_request() {
1966 let mut exec = fasync::TestExecutor::new();
1967
1968 let candidate = generate_random_scanned_candidate();
1970 let connection_selector = Rc::new(FakeConnectionSelector {
1971 response_to_find_and_select_connection_candidate: None,
1972 response_to_find_and_select_roam_candidate: Some(candidate.clone()),
1973 });
1974
1975 let (request_sender, request_receiver) = mpsc::channel(5);
1977 let power_manager = Arc::new(wlan_power_manager_testing::TestPowerManager::new());
1978 let mut serve_fut = pin!(serve_connection_selection_request_loop(
1979 connection_selector,
1980 request_receiver,
1981 power_manager.clone(),
1982 ));
1983 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
1984
1985 let mut requester = ConnectionSelectionRequester { sender: request_sender };
1987
1988 let bss_desc =
1990 BssDescription::try_from(Sequestered::release(candidate.bss.bss_description.clone()))
1991 .unwrap();
1992 let mut roam_selection_fut = pin!(requester.do_roam_selection(
1993 fidl_common::ScanType::Passive,
1994 candidate.network.clone(),
1995 candidate.credential.clone(),
1996 bss_desc.protection().into()
1997 ));
1998 assert_matches!(exec.run_until_stalled(&mut roam_selection_fut), Poll::Pending);
1999
2000 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
2002
2003 assert_matches!(exec.run_until_stalled(&mut roam_selection_fut), Poll::Ready(Ok(Some(selected_candidate))) => {
2005 assert_eq!(selected_candidate, candidate);
2006 });
2007
2008 let calls = power_manager.calls.lock();
2009 assert!(calls.contains(&"wlancfg-roam-selection".to_string()));
2010 }
2011}