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