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_contrib::inspect_insert;
18use fuchsia_inspect_contrib::log::WriteInspect;
19use fuchsia_inspect_contrib::nodes::BoundedListNode as InspectBoundedListNode;
20use futures::channel::{mpsc, oneshot};
21use futures::lock::Mutex;
22use futures::select;
23use futures::stream::StreamExt;
24use log::{debug, error, info, warn};
25use std::borrow::Cow;
26use std::collections::{HashMap, HashSet};
27use std::rc::Rc;
28use std::sync::Arc;
29use wlan_common::security::SecurityAuthenticator;
30use wlan_common::sequestered::Sequestered;
31use {fidl_fuchsia_wlan_common as fidl_common, fuchsia_async as fasync};
32
33pub mod bss_selection;
34pub mod fut_manager;
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<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 telemetry_sender: TelemetrySender,
170 ) -> Self {
171 let inspect_node_for_connection_selection = InspectBoundedListNode::new(
172 inspect_node.create_child("connection_selection"),
173 INSPECT_EVENT_LIMIT_FOR_CONNECTION_SELECTIONS,
174 );
175 Self {
176 saved_network_manager,
177 scan_requester,
178 last_scan_result_time: Arc::new(Mutex::new(zx::MonotonicInstant::ZERO)),
179 _inspect_node_root: Arc::new(Mutex::new(inspect_node)),
180 inspect_node_for_connection_selection: Arc::new(Mutex::new(
181 inspect_node_for_connection_selection,
182 )),
183 telemetry_sender,
184 }
185 }
186
187 async fn find_available_bss_candidate_list(
190 &self,
191 network: Option<types::NetworkIdentifier>,
192 ) -> Vec<types::ScannedCandidate> {
193 let scan_for_candidates = || async {
194 if let Some(network) = &network {
195 self.scan_requester
196 .perform_scan(ScanReason::BssSelection, vec![network.ssid.clone()], vec![])
197 .await
198 } else {
199 let last_scan_result_time = *self.last_scan_result_time.lock().await;
200 let scan_age = zx::MonotonicInstant::get() - last_scan_result_time;
201 if last_scan_result_time != zx::MonotonicInstant::ZERO {
202 info!("Scan results are {}s old, triggering a scan", scan_age.into_seconds());
203 self.telemetry_sender.send(TelemetryEvent::NetworkSelectionScanInterval {
204 time_since_last_scan: scan_age,
205 });
206 }
207 let passive_scan_results = match self
208 .scan_requester
209 .perform_scan(ScanReason::NetworkSelection, vec![], vec![])
210 .await
211 {
212 Ok(scan_results) => scan_results,
213 Err(e) => return Err(e),
214 };
215 let passive_scan_ssids: HashSet<types::Ssid> = HashSet::from_iter(
216 passive_scan_results.iter().map(|result| result.ssid.clone()),
217 );
218 let requested_active_scan_ssids: Vec<types::Ssid> =
219 select_subset_potentially_hidden_networks(
220 self.saved_network_manager.get_networks().await,
221 )
222 .drain(..)
223 .map(|id| id.ssid)
224 .filter(|ssid| !passive_scan_ssids.contains(ssid))
225 .collect();
226
227 self.telemetry_sender.send(TelemetryEvent::ActiveScanRequested {
228 num_ssids_requested: requested_active_scan_ssids.len(),
229 });
230
231 if requested_active_scan_ssids.is_empty() {
232 Ok(passive_scan_results)
233 } else {
234 self.scan_requester
235 .perform_scan(
236 ScanReason::NetworkSelection,
237 requested_active_scan_ssids,
238 vec![],
239 )
240 .await
241 .map(|mut scan_results| {
242 scan_results.extend(passive_scan_results);
243 scan_results
244 })
245 }
246 }
247 };
248
249 let scan_results = scan_for_candidates().await;
250
251 match scan_results {
252 Err(e) => {
253 warn!("Failed to get available BSSs, {:?}", e);
254 vec![]
255 }
256 Ok(scan_results) => {
257 let candidates =
258 merge_saved_networks_and_scan_data(&self.saved_network_manager, scan_results)
259 .await;
260 if network.is_none() {
261 *self.last_scan_result_time.lock().await = zx::MonotonicInstant::get();
262 record_metrics_on_scan(candidates.clone(), &self.telemetry_sender);
263 }
264 candidates
265 }
266 }
267 }
268
269 async fn augment_bss_candidate_with_active_scan(
272 &self,
273 scanned_candidate: types::ScannedCandidate,
274 ) -> types::ScannedCandidate {
275 async fn get_enhanced_bss_description(
278 scanned_candidate: &types::ScannedCandidate,
279 scan_requester: Arc<dyn scan::ScanRequestApi>,
280 ) -> Result<Sequestered<fidl_common::BssDescription>, ()> {
281 match scanned_candidate.bss.observation {
282 types::ScanObservation::Passive => {
283 info!("Performing directed active scan on selected network")
284 }
285 types::ScanObservation::Active => {
286 debug!("Network already discovered via active scan.");
287 return Err(());
288 }
289 types::ScanObservation::Unknown => {
290 error!("Unexpected `Unknown` variant of network `observation`.");
291 return Err(());
292 }
293 }
294
295 let mut directed_scan_result = scan_requester
297 .perform_scan(
298 ScanReason::BssSelectionAugmentation,
299 vec![scanned_candidate.network.ssid.clone()],
300 vec![scanned_candidate.bss.channel],
301 )
302 .await
303 .map_err(|_| {
304 info!("Failed to perform active scan to augment BSS info.");
305 })?;
306
307 let bss_description = directed_scan_result
309 .drain(..)
310 .find_map(|mut network| {
311 if network.ssid == scanned_candidate.network.ssid {
312 for bss in network.entries.drain(..) {
313 if bss.bssid == scanned_candidate.bss.bssid {
314 return Some(bss.bss_description);
315 }
316 }
317 }
318 None
319 })
320 .ok_or_else(|| {
321 info!("BSS info will lack active scan augmentation, proceeding anyway.");
322 })?;
323
324 Ok(bss_description)
325 }
326
327 match get_enhanced_bss_description(&scanned_candidate, self.scan_requester.clone()).await {
328 Ok(new_bss_description) => {
329 let updated_scanned_bss =
330 Bss { bss_description: new_bss_description, ..scanned_candidate.bss.clone() };
331 types::ScannedCandidate { bss: updated_scanned_bss, ..scanned_candidate }
332 }
333 Err(()) => scanned_candidate,
334 }
335 }
336
337 async fn roam_scan(
339 &self,
340 scan_type: fidl_common::ScanType,
341 network: types::NetworkIdentifier,
342 current_security: types::SecurityTypeDetailed,
343 ) -> Vec<types::ScanResult> {
344 let ssids = match scan_type {
345 fidl_common::ScanType::Passive => vec![],
346 fidl_common::ScanType::Active => vec![network.ssid.clone()],
347 };
348 self.scan_requester
349 .perform_scan(ScanReason::RoamSearch, ssids, vec![])
350 .await
351 .unwrap_or_else(|e| {
352 error!("{}", format_err!("Error scanning: {:?}", e));
353 vec![]
354 })
355 .into_iter()
356 .filter(|s| {
357 s.ssid == network.ssid && current_security == s.security_type_detailed
359 })
360 .collect::<Vec<_>>()
361 }
362}
363
364#[async_trait(?Send)]
365impl ConnectionSelectorApi for ConnectionSelector {
366 async fn find_and_select_connection_candidate(
371 &self,
372 network: Option<types::NetworkIdentifier>,
373 reason: types::ConnectReason,
374 ) -> Option<types::ScannedCandidate> {
375 let available_candidate_list =
377 self.find_available_bss_candidate_list(network.clone()).await;
378
379 let available_networks: HashSet<types::NetworkIdentifier> =
381 available_candidate_list.iter().map(|candidate| candidate.network.clone()).collect();
382 let selected_networks = network_selection::select_networks(available_networks, &network);
383
384 self.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
386 network_selection_type: match network {
387 Some(_) => telemetry::NetworkSelectionType::Directed,
388 None => telemetry::NetworkSelectionType::Undirected,
389 },
390 num_candidates: (!available_candidate_list.is_empty())
391 .then_some(available_candidate_list.len())
392 .ok_or(()),
393 selected_count: selected_networks.len(),
394 });
395
396 let allowed_candidate_list = available_candidate_list
398 .iter()
399 .filter(|candidate| selected_networks.contains(&candidate.network))
400 .cloned()
401 .collect();
402
403 match bss_selection::select_bss(
406 allowed_candidate_list,
407 reason,
408 self.inspect_node_for_connection_selection.clone(),
409 self.telemetry_sender.clone(),
410 )
411 .await
412 {
413 Some(mut candidate) => {
414 if network.is_some() {
415 candidate.bss.observation = types::ScanObservation::Unknown;
419 }
420 match candidate.bss.observation {
422 types::ScanObservation::Passive => {
423 Some(self.augment_bss_candidate_with_active_scan(candidate.clone()).await)
424 }
425 _ => Some(candidate),
426 }
427 }
428 None => None,
429 }
430 }
431
432 async fn find_and_select_roam_candidate(
436 &self,
437 scan_type: fidl_common::ScanType,
438 network: types::NetworkIdentifier,
439 credential: &network_config::Credential,
440 current_security: types::SecurityTypeDetailed,
441 ) -> Option<types::ScannedCandidate> {
442 let mut matching_scan_results =
444 self.roam_scan(scan_type, network.clone(), current_security).await;
445 if matching_scan_results.is_empty() && scan_type == fidl_common::ScanType::Passive {
446 info!("No scan results seen in passive roam scan. Active scanning.");
447 matching_scan_results = self
448 .roam_scan(fidl_common::ScanType::Active, network.clone(), current_security)
449 .await;
450 }
451
452 let mut candidates = Vec::new();
453 for mut s in matching_scan_results {
456 if let Some(config) = self
457 .saved_network_manager
458 .lookup(&network)
459 .await
460 .into_iter()
461 .find(|c| &c.credential == credential)
462 {
463 candidates.append(&mut merge_config_and_scan_data(config, &mut s));
464 } else {
465 warn!("Failed to find config for network to roam from");
468 }
469 }
470 bss_selection::select_bss(
472 candidates,
473 types::ConnectReason::ProactiveNetworkSwitch,
474 self.inspect_node_for_connection_selection.clone(),
475 self.telemetry_sender.clone(),
476 )
477 .await
478 }
479}
480
481impl types::ScannedCandidate {
482 pub fn recent_failure_count(&self) -> u64 {
483 self.saved_network_info
484 .recent_failures
485 .iter()
486 .filter(|failure| failure.bssid == self.bss.bssid)
487 .count()
488 .try_into()
489 .unwrap_or_else(|e| {
490 error!("{}", e);
491 u64::MAX
492 })
493 }
494 pub fn recent_short_connections(&self) -> usize {
495 self.saved_network_info
496 .past_connections
497 .get_list_for_bss(&self.bss.bssid)
498 .get_recent(fasync::MonotonicInstant::now() - RECENT_DISCONNECT_WINDOW)
499 .iter()
500 .filter(|d| d.connection_uptime < SHORT_CONNECT_DURATION)
501 .count()
502 }
503
504 pub fn saved_security_type_to_string(&self) -> String {
505 match self.network.security_type {
506 SecurityType::None => "open",
507 SecurityType::Wep => "WEP",
508 SecurityType::Wpa => "WPA1",
509 SecurityType::Wpa2 => "WPA2",
510 SecurityType::Wpa3 => "WPA3",
511 }
512 .to_string()
513 }
514
515 pub fn scanned_security_type_to_string(&self) -> String {
516 match self.security_type_detailed {
517 SecurityTypeDetailed::Unknown => "unknown",
518 SecurityTypeDetailed::Open => "open",
519 SecurityTypeDetailed::OpenOweTransition => "Open OWE Transition",
520 SecurityTypeDetailed::Owe => "OWE",
521 SecurityTypeDetailed::Wep => "WEP",
522 SecurityTypeDetailed::Wpa1 => "WPA1",
523 SecurityTypeDetailed::Wpa1Wpa2PersonalTkipOnly => "WPA1/2Tk",
524 SecurityTypeDetailed::Wpa2PersonalTkipOnly => "WPA2Tk",
525 SecurityTypeDetailed::Wpa1Wpa2Personal => "WPA1/2",
526 SecurityTypeDetailed::Wpa2Personal => "WPA2",
527 SecurityTypeDetailed::Wpa2Wpa3Personal => "WPA2/3",
528 SecurityTypeDetailed::Wpa3Personal => "WPA3",
529 SecurityTypeDetailed::Wpa2Enterprise => "WPA2Ent",
530 SecurityTypeDetailed::Wpa3Enterprise => "WPA3Ent",
531 }
532 .to_string()
533 }
534
535 pub fn to_string_without_pii(&self) -> String {
536 let channel = self.bss.channel;
537 let rssi = self.bss.signal.rssi_dbm;
538 let recent_failure_count = self.recent_failure_count();
539 let recent_short_connection_count = self.recent_short_connections();
540
541 format!(
542 "{}({:4}), {}({:6}), {:>4}dBm, channel {:8}, score {:4}{}{}{}{}",
543 self.network.ssid,
544 self.saved_security_type_to_string(),
545 self.bss.bssid,
546 self.scanned_security_type_to_string(),
547 rssi,
548 channel,
549 scoring_functions::score_bss_scanned_candidate(self.clone()),
550 if !self.bss.is_compatible() { ", NOT compatible" } else { "" },
551 if recent_failure_count > 0 {
552 format!(", {recent_failure_count} recent failures")
553 } else {
554 "".to_string()
555 },
556 if recent_short_connection_count > 0 {
557 format!(", {recent_short_connection_count} recent short disconnects")
558 } else {
559 "".to_string()
560 },
561 if !self.saved_network_info.has_ever_connected { ", never used yet" } else { "" },
562 )
563 }
564}
565
566impl WriteInspect for types::ScannedCandidate {
567 fn write_inspect<'a>(&self, writer: &InspectNode, key: impl Into<Cow<'a, str>>) {
568 inspect_insert!(writer, var key: {
569 ssid: self.network.ssid.to_string(),
570 bssid: self.bss.bssid.to_string(),
571 rssi: self.bss.signal.rssi_dbm,
572 score: scoring_functions::score_bss_scanned_candidate(self.clone()),
573 security_type_saved: self.saved_security_type_to_string(),
574 security_type_scanned: format!(
575 "{}",
576 wlan_common::bss::Protection::from(self.security_type_detailed),
577 ),
578 channel: format!("{}", self.bss.channel),
579 compatible: self.bss.is_compatible(),
580 incompatibility: self.bss
581 .compatibility
582 .as_ref()
583 .err()
584 .map(ToString::to_string)
585 .unwrap_or_else(|| String::from("none")),
586 recent_failure_count: self.recent_failure_count(),
587 saved_network_has_ever_connected: self.saved_network_info.has_ever_connected,
588 });
589 }
590}
591
592fn get_authenticator(bss: &Bss, credential: &Credential) -> Option<SecurityAuthenticator> {
593 let mutual_security_protocols = match bss.compatibility.as_ref() {
594 Ok(compatible) => compatible.mutual_security_protocols().clone(),
595 Err(incompatible) => {
596 error!("BSS ({:?}) is incompatible: {}", bss.bssid, incompatible);
597 return None;
598 }
599 };
600
601 match select_authentication_method(mutual_security_protocols.clone(), credential) {
602 Some(authenticator) => Some(authenticator),
603 None => {
604 error!(
605 "Failed to negotiate authentication for BSS ({:?}) with mutually supported \
606 security protocols: {:?}, and credential type: {:?}.",
607 bss.bssid,
608 mutual_security_protocols,
609 credential.type_str()
610 );
611 None
612 }
613 }
614}
615
616fn merge_config_and_scan_data(
617 network_config: config_management::NetworkConfig,
618 scan_result: &mut types::ScanResult,
619) -> Vec<types::ScannedCandidate> {
620 if network_config.ssid != scan_result.ssid
621 || !network_config
622 .security_type
623 .is_compatible_with_scanned_type(&scan_result.security_type_detailed)
624 {
625 return Vec::new();
626 }
627
628 let mut merged_networks = Vec::new();
629 let multiple_bss_candidates = scan_result.entries.len() > 1;
630
631 for bss in scan_result.entries.iter() {
632 let authenticator = match get_authenticator(bss, &network_config.credential) {
633 Some(authenticator) => authenticator,
634 None => {
635 error!(
636 "Failed to create authenticator for bss candidate {:?} (SSID: {:?}). Removing from candidates.",
637 bss.bssid, &network_config.ssid
638 );
639 continue;
640 }
641 };
642 let scanned_candidate = types::ScannedCandidate {
643 network: types::NetworkIdentifier {
644 ssid: network_config.ssid.clone(),
645 security_type: network_config.security_type,
646 },
647 security_type_detailed: scan_result.security_type_detailed,
648 credential: network_config.credential.clone(),
649 network_has_multiple_bss: multiple_bss_candidates,
650 saved_network_info: InternalSavedNetworkData {
651 has_ever_connected: network_config.has_ever_connected,
652 recent_failures: network_config.perf_stats.connect_failures.get_recent_for_network(
653 fasync::MonotonicInstant::now() - RECENT_FAILURE_WINDOW,
654 ),
655 past_connections: network_config.perf_stats.past_connections.clone(),
656 },
657 bss: bss.clone(),
658 authenticator,
659 };
660 merged_networks.push(scanned_candidate)
661 }
662 merged_networks
663}
664
665async fn merge_saved_networks_and_scan_data(
668 saved_network_manager: &Arc<dyn SavedNetworksManagerApi>,
669 mut scan_results: Vec<types::ScanResult>,
670) -> Vec<types::ScannedCandidate> {
671 let mut merged_networks = vec![];
672 for mut scan_result in scan_results.drain(..) {
673 for saved_config in saved_network_manager
674 .lookup_compatible(&scan_result.ssid, scan_result.security_type_detailed)
675 .await
676 {
677 let multiple_bss_candidates = scan_result.entries.len() > 1;
678 for bss in scan_result.entries.drain(..) {
679 let authenticator = match get_authenticator(&bss, &saved_config.credential) {
680 Some(authenticator) => authenticator,
681 None => {
682 error!(
683 "Failed to create authenticator for bss candidate {} (SSID: {}). Removing from candidates.",
684 bss.bssid, saved_config.ssid
685 );
686 continue;
687 }
688 };
689 let scanned_candidate = types::ScannedCandidate {
690 network: types::NetworkIdentifier {
691 ssid: saved_config.ssid.clone(),
692 security_type: saved_config.security_type,
693 },
694 security_type_detailed: scan_result.security_type_detailed,
695 credential: saved_config.credential.clone(),
696 network_has_multiple_bss: multiple_bss_candidates,
697 saved_network_info: InternalSavedNetworkData {
698 has_ever_connected: saved_config.has_ever_connected,
699 recent_failures: saved_config
700 .perf_stats
701 .connect_failures
702 .get_recent_for_network(
703 fasync::MonotonicInstant::now() - RECENT_FAILURE_WINDOW,
704 ),
705 past_connections: saved_config.perf_stats.past_connections.clone(),
706 },
707 bss,
708 authenticator,
709 };
710 merged_networks.push(scanned_candidate)
711 }
712 }
713 }
714 merged_networks
715}
716
717fn record_metrics_on_scan(
718 mut merged_networks: Vec<types::ScannedCandidate>,
719 telemetry_sender: &TelemetrySender,
720) {
721 let mut merged_network_map: HashMap<types::NetworkIdentifier, Vec<types::ScannedCandidate>> =
722 HashMap::new();
723 for bss in merged_networks.drain(..) {
724 merged_network_map.entry(bss.network.clone()).or_default().push(bss);
725 }
726
727 let num_saved_networks_observed = merged_network_map.len();
728 let mut num_actively_scanned_networks = 0;
729 let bss_count_per_saved_network = merged_network_map
730 .values()
731 .map(|bsss| {
732 if bsss.iter().any(|bss| matches!(bss.bss.observation, types::ScanObservation::Active))
734 {
735 num_actively_scanned_networks += 1;
736 };
737 bsss.len()
739 })
740 .collect();
741
742 telemetry_sender.send(TelemetryEvent::ConnectionSelectionScanResults {
743 saved_network_count: num_saved_networks_observed,
744 bss_count_per_saved_network,
745 saved_network_count_found_by_active_scan: num_actively_scanned_networks,
746 });
747}
748#[cfg(test)]
749mod tests {
750 use super::*;
751 use crate::config_management::network_config::HistoricalListsByBssid;
752 use crate::config_management::{ConnectFailure, FailureReason, SavedNetworksManager};
753 use crate::util::testing::fakes::{FakeSavedNetworksManager, FakeScanRequester};
754 use crate::util::testing::{
755 generate_channel, generate_random_bss, generate_random_connect_reason,
756 generate_random_network_identifier, generate_random_password, generate_random_scan_result,
757 generate_random_scanned_candidate,
758 };
759 use assert_matches::assert_matches;
760 use diagnostics_assertions::{AnyNumericProperty, assert_data_tree};
761 use futures::task::Poll;
762 use ieee80211_testutils::BSSID_REGEX;
763 use rand::Rng;
764 use std::pin::pin;
765 use std::rc::Rc;
766 use std::sync::LazyLock;
767 use test_case::test_case;
768 use wlan_common::bss::BssDescription;
769 use wlan_common::random_fidl_bss_description;
770 use wlan_common::scan::Compatible;
771 use wlan_common::security::SecurityDescriptor;
772 use {
773 fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211, fidl_fuchsia_wlan_sme as fidl_sme,
774 fuchsia_async as fasync, fuchsia_inspect as inspect,
775 };
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 failure_time = test_values
930 .real_saved_network_manager
931 .lookup(&test_id_1.clone())
932 .await
933 .first()
934 .expect("failed to get config")
935 .perf_stats
936 .connect_failures
937 .get_recent_for_network(fasync::MonotonicInstant::now() - RECENT_FAILURE_WINDOW)
938 .first()
939 .expect("failed to get recent failure")
940 .time;
941 let recent_failures = vec![ConnectFailure {
942 bssid: bssid_2,
943 time: failure_time,
944 reason: FailureReason::CredentialRejected,
945 }];
946 let expected_internal_data_1 = InternalSavedNetworkData {
947 has_ever_connected: true,
948 recent_failures: recent_failures.clone(),
949 past_connections: HistoricalListsByBssid::new(),
950 };
951 let wpa3_authenticator = select_authentication_method(
952 HashSet::from([SecurityDescriptor::WPA3_PERSONAL]),
953 &credential_1,
954 )
955 .unwrap();
956 let open_authenticator =
957 select_authentication_method(HashSet::from([SecurityDescriptor::WPA1]), &credential_2)
958 .unwrap();
959 let expected_results = vec![
960 types::ScannedCandidate {
961 network: test_id_1.clone(),
962 credential: credential_1.clone(),
963 network_has_multiple_bss: true,
964 security_type_detailed: test_security_1,
965 saved_network_info: expected_internal_data_1.clone(),
966 bss: mock_scan_results[0].entries[0].clone(),
967 authenticator: wpa3_authenticator.clone(),
968 },
969 types::ScannedCandidate {
970 network: test_id_1.clone(),
971 credential: credential_1.clone(),
972 network_has_multiple_bss: true,
973 security_type_detailed: test_security_1,
974 saved_network_info: expected_internal_data_1.clone(),
975 bss: mock_scan_results[0].entries[1].clone(),
976 authenticator: wpa3_authenticator.clone(),
977 },
978 types::ScannedCandidate {
979 network: test_id_1.clone(),
980 credential: credential_1.clone(),
981 network_has_multiple_bss: true,
982 security_type_detailed: test_security_1,
983 saved_network_info: expected_internal_data_1.clone(),
984 bss: mock_scan_results[0].entries[2].clone(),
985 authenticator: wpa3_authenticator.clone(),
986 },
987 types::ScannedCandidate {
988 network: test_id_2.clone(),
989 credential: credential_2.clone(),
990 network_has_multiple_bss: false,
991 security_type_detailed: test_security_2,
992 saved_network_info: InternalSavedNetworkData {
993 has_ever_connected: false,
994 recent_failures: Vec::new(),
995 past_connections: HistoricalListsByBssid::new(),
996 },
997 bss: mock_scan_results[1].entries[0].clone(),
998 authenticator: open_authenticator.clone(),
999 },
1000 ];
1001
1002 let results = merge_saved_networks_and_scan_data(
1004 #[allow(clippy::arc_with_non_send_sync)]
1006 &Arc::new(test_values.real_saved_network_manager),
1007 mock_scan_results,
1008 )
1009 .await;
1010
1011 assert_eq!(results, expected_results);
1012 }
1013
1014 #[fuchsia::test]
1015 fn augment_bss_candidate_with_active_scan_doesnt_run_on_actively_found_networks() {
1016 let mut exec = fasync::TestExecutor::new();
1017 let test_values = exec.run_singlethreaded(test_setup(true));
1018 let mut candidate = generate_random_scanned_candidate();
1019 candidate.bss.observation = types::ScanObservation::Active;
1020
1021 let fut = test_values
1022 .connection_selector
1023 .augment_bss_candidate_with_active_scan(candidate.clone());
1024 let mut fut = pin!(fut);
1025
1026 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(res) => {
1028 assert_eq!(&res, &candidate);
1029 });
1030 }
1031
1032 #[fuchsia::test]
1033 fn augment_bss_candidate_with_active_scan_runs_on_passively_found_networks() {
1034 let mut exec = fasync::TestExecutor::new();
1035 let test_values = exec.run_singlethreaded(test_setup(true));
1036
1037 let mut passively_scanned_candidate = generate_random_scanned_candidate();
1038 passively_scanned_candidate.bss.observation = types::ScanObservation::Passive;
1039
1040 let fut = test_values
1041 .connection_selector
1042 .augment_bss_candidate_with_active_scan(passively_scanned_candidate.clone());
1043 let fut = pin!(fut);
1044
1045 let new_bss_desc = random_fidl_bss_description!();
1047 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1048 types::ScanResult {
1049 ssid: passively_scanned_candidate.network.ssid.clone(),
1050 security_type_detailed: passively_scanned_candidate.security_type_detailed,
1051 compatibility: types::Compatibility::Supported,
1052 entries: vec![types::Bss {
1053 bssid: passively_scanned_candidate.bss.bssid,
1054 compatibility: wlan_common::scan::Compatible::expect_ok([
1055 wlan_common::security::SecurityDescriptor::WPA1,
1056 ]),
1057 bss_description: new_bss_desc.clone().into(),
1058 ..generate_random_bss()
1059 }],
1060 },
1061 ])));
1062
1063 let candidate = exec.run_singlethreaded(fut);
1064 assert_eq!(
1066 &candidate,
1067 &types::ScannedCandidate {
1068 bss: types::Bss {
1069 bss_description: new_bss_desc.into(),
1070 ..passively_scanned_candidate.bss.clone()
1071 },
1072 ..passively_scanned_candidate.clone()
1073 },
1074 );
1075
1076 assert_eq!(
1078 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1079 vec![(
1080 ScanReason::BssSelectionAugmentation,
1081 vec![passively_scanned_candidate.network.ssid.clone()],
1082 vec![passively_scanned_candidate.bss.channel]
1083 )]
1084 );
1085 }
1086
1087 #[fuchsia::test]
1088 fn find_available_bss_list_with_network_specified() {
1089 let mut exec = fasync::TestExecutor::new();
1090 let test_values = exec.run_singlethreaded(test_setup(false));
1091 let connection_selector = test_values.connection_selector;
1092
1093 let test_id_1 = types::NetworkIdentifier {
1095 ssid: types::Ssid::try_from("foo").unwrap(),
1096 security_type: types::SecurityType::Wpa3,
1097 };
1098 let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
1099
1100 assert!(
1102 exec.run_singlethreaded(
1103 test_values.saved_network_manager.store(test_id_1.clone(), credential_1.clone()),
1104 )
1105 .unwrap()
1106 .is_none()
1107 );
1108
1109 let mutual_security_protocols_1 = [SecurityDescriptor::WPA3_PERSONAL];
1111 let bss_desc_1 = random_fidl_bss_description!();
1112 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1113 types::ScanResult {
1114 ssid: test_id_1.ssid.clone(),
1115 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1116 compatibility: types::Compatibility::Supported,
1117 entries: vec![types::Bss {
1118 compatibility: wlan_common::scan::Compatible::expect_ok(
1119 mutual_security_protocols_1,
1120 ),
1121 bss_description: bss_desc_1.clone().into(),
1122 ..generate_random_bss()
1123 }],
1124 },
1125 generate_random_scan_result(),
1126 generate_random_scan_result(),
1127 ])));
1128
1129 let fut = connection_selector.find_available_bss_candidate_list(Some(test_id_1.clone()));
1131 let mut fut = pin!(fut);
1132 let results = exec.run_singlethreaded(&mut fut);
1133 assert_eq!(results.len(), 1);
1134
1135 assert_eq!(
1137 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1138 vec![(ScanReason::BssSelection, vec![test_id_1.ssid.clone()], vec![])]
1139 );
1140 }
1141
1142 #[test_case(true)]
1143 #[test_case(false)]
1144 #[fuchsia::test(add_test_attr = false)]
1145 fn find_available_bss_list_without_network_specified(hidden: bool) {
1146 let mut exec = fasync::TestExecutor::new();
1147 let mut test_values = exec.run_singlethreaded(test_setup(false));
1148 let connection_selector = test_values.connection_selector;
1149
1150 let test_id_not_hidden = types::NetworkIdentifier {
1152 ssid: types::Ssid::try_from("foo").unwrap(),
1153 security_type: types::SecurityType::Wpa3,
1154 };
1155 let test_id_maybe_hidden = types::NetworkIdentifier {
1156 ssid: types::Ssid::try_from("bar").unwrap(),
1157 security_type: types::SecurityType::Wpa3,
1158 };
1159 let test_id_hidden_but_seen = types::NetworkIdentifier {
1160 ssid: types::Ssid::try_from("baz").unwrap(),
1161 security_type: types::SecurityType::Wpa3,
1162 };
1163 let credential = Credential::Password("some_pass".as_bytes().to_vec());
1164
1165 assert!(
1167 exec.run_singlethreaded(
1168 test_values
1169 .saved_network_manager
1170 .store(test_id_not_hidden.clone(), credential.clone()),
1171 )
1172 .unwrap()
1173 .is_none()
1174 );
1175 assert!(
1176 exec.run_singlethreaded(
1177 test_values
1178 .saved_network_manager
1179 .store(test_id_maybe_hidden.clone(), credential.clone()),
1180 )
1181 .unwrap()
1182 .is_none()
1183 );
1184 assert!(
1185 exec.run_singlethreaded(
1186 test_values
1187 .saved_network_manager
1188 .store(test_id_hidden_but_seen.clone(), credential.clone()),
1189 )
1190 .unwrap()
1191 .is_none()
1192 );
1193
1194 exec.run_singlethreaded(
1196 test_values.saved_network_manager.update_hidden_prob(test_id_not_hidden.clone(), 0.0),
1197 );
1198 exec.run_singlethreaded(
1199 test_values
1200 .saved_network_manager
1201 .update_hidden_prob(test_id_hidden_but_seen.clone(), 1.0),
1202 );
1203 exec.run_singlethreaded(
1205 test_values
1206 .saved_network_manager
1207 .update_hidden_prob(test_id_maybe_hidden.clone(), if hidden { 1.0 } else { 0.0 }),
1208 );
1209
1210 let mutual_security_protocols = [SecurityDescriptor::WPA3_PERSONAL];
1212 let bss_desc = random_fidl_bss_description!();
1213 if hidden {
1214 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1216 types::ScanResult {
1217 ssid: test_id_not_hidden.ssid.clone(),
1218 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1219 compatibility: types::Compatibility::Supported,
1220 entries: vec![types::Bss {
1221 compatibility: wlan_common::scan::Compatible::expect_ok(
1222 mutual_security_protocols,
1223 ),
1224 bss_description: bss_desc.clone().into(),
1225 ..generate_random_bss()
1226 }],
1227 },
1228 types::ScanResult {
1229 ssid: test_id_hidden_but_seen.ssid.clone(),
1230 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1231 compatibility: types::Compatibility::Supported,
1232 entries: vec![types::Bss {
1233 compatibility: wlan_common::scan::Compatible::expect_ok(
1234 mutual_security_protocols,
1235 ),
1236 bss_description: bss_desc.clone().into(),
1237 ..generate_random_bss()
1238 }],
1239 },
1240 generate_random_scan_result(),
1241 generate_random_scan_result(),
1242 ])));
1243 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1245 types::ScanResult {
1246 ssid: test_id_maybe_hidden.ssid.clone(),
1247 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1248 compatibility: types::Compatibility::Supported,
1249 entries: vec![types::Bss {
1250 compatibility: wlan_common::scan::Compatible::expect_ok(
1251 mutual_security_protocols,
1252 ),
1253 bss_description: bss_desc.clone().into(),
1254 ..generate_random_bss()
1255 }],
1256 },
1257 generate_random_scan_result(),
1258 generate_random_scan_result(),
1259 ])));
1260 } else {
1261 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1263 types::ScanResult {
1264 ssid: test_id_not_hidden.ssid.clone(),
1265 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1266 compatibility: types::Compatibility::Supported,
1267 entries: vec![types::Bss {
1268 compatibility: wlan_common::scan::Compatible::expect_ok(
1269 mutual_security_protocols,
1270 ),
1271 bss_description: bss_desc.clone().into(),
1272 ..generate_random_bss()
1273 }],
1274 },
1275 types::ScanResult {
1276 ssid: test_id_maybe_hidden.ssid.clone(),
1277 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1278 compatibility: types::Compatibility::Supported,
1279 entries: vec![types::Bss {
1280 compatibility: wlan_common::scan::Compatible::expect_ok(
1281 mutual_security_protocols,
1282 ),
1283 bss_description: bss_desc.clone().into(),
1284 ..generate_random_bss()
1285 }],
1286 },
1287 types::ScanResult {
1288 ssid: test_id_hidden_but_seen.ssid.clone(),
1289 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1290 compatibility: types::Compatibility::Supported,
1291 entries: vec![types::Bss {
1292 compatibility: wlan_common::scan::Compatible::expect_ok(
1293 mutual_security_protocols,
1294 ),
1295 bss_description: bss_desc.clone().into(),
1296 ..generate_random_bss()
1297 }],
1298 },
1299 generate_random_scan_result(),
1300 generate_random_scan_result(),
1301 ])));
1302 }
1303
1304 let connection_selection_fut = connection_selector.find_available_bss_candidate_list(None);
1306 let mut connection_selection_fut = pin!(connection_selection_fut);
1307 let results = exec.run_singlethreaded(&mut connection_selection_fut);
1308 assert_eq!(results.len(), 3);
1309
1310 if hidden {
1313 assert_eq!(
1314 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1315 vec![
1316 (ScanReason::NetworkSelection, vec![], vec![]),
1317 (ScanReason::NetworkSelection, vec![test_id_maybe_hidden.ssid.clone()], vec![])
1318 ]
1319 )
1320 } else {
1321 assert_eq!(
1322 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1323 vec![(ScanReason::NetworkSelection, vec![], vec![])]
1324 )
1325 }
1326
1327 assert_matches!(
1329 test_values.telemetry_receiver.try_next(),
1330 Ok(Some(TelemetryEvent::ActiveScanRequested{num_ssids_requested})) => {
1331 if hidden {
1332 assert_eq!(num_ssids_requested, 1);
1333 } else {
1334 assert_eq!(num_ssids_requested, 0);
1335 }
1336 });
1337 }
1338
1339 #[fuchsia::test]
1340 fn find_and_select_connection_candidate_scan_error() {
1341 let mut exec = fasync::TestExecutor::new();
1342 let test_values = exec.run_singlethreaded(test_setup(true));
1343 let connection_selector = test_values.connection_selector;
1344 let mut telemetry_receiver = test_values.telemetry_receiver;
1345
1346 exec.run_singlethreaded(
1348 test_values.scan_requester.add_scan_result(Err(types::ScanError::GeneralError)),
1349 );
1350
1351 let mut connection_selection_fut = pin!(
1353 connection_selector
1354 .find_and_select_connection_candidate(None, generate_random_connect_reason())
1355 );
1356 assert_matches!(exec.run_until_stalled(&mut connection_selection_fut), Poll::Ready(None));
1358
1359 assert_eq!(
1361 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1362 vec![(scan::ScanReason::NetworkSelection, vec![], vec![])]
1363 );
1364
1365 assert_data_tree!(@executor exec, test_values.inspector, root: {
1367 connection_selection_test: {
1368 connection_selection: {
1369 "0": {
1370 "@time": AnyNumericProperty,
1371 "candidates": {},
1372 },
1373 }
1374 },
1375 });
1376
1377 assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
1379 assert_matches!(event, TelemetryEvent::NetworkSelectionDecision {
1380 network_selection_type: telemetry::NetworkSelectionType::Undirected,
1381 num_candidates: Err(()),
1382 selected_count: 0,
1383 });
1384 });
1385 assert_matches!(
1386 telemetry_receiver.try_next(),
1387 Ok(Some(TelemetryEvent::BssSelectionResult { selected_candidate: None, .. }))
1388 );
1389 }
1390
1391 #[fuchsia::test]
1392 fn find_and_select_connection_candidate_end_to_end() {
1393 let mut exec = fasync::TestExecutor::new();
1394 let test_values = exec.run_singlethreaded(test_setup(true));
1395 let connection_selector = test_values.connection_selector;
1396 let mut telemetry_receiver = test_values.telemetry_receiver;
1397
1398 let test_id_1 = types::NetworkIdentifier {
1400 ssid: types::Ssid::try_from("foo").unwrap(),
1401 security_type: types::SecurityType::Wpa3,
1402 };
1403 let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
1404 let bssid_1 = types::Bssid::from([1, 1, 1, 1, 1, 1]);
1405
1406 let test_id_2 = types::NetworkIdentifier {
1407 ssid: types::Ssid::try_from("bar").unwrap(),
1408 security_type: types::SecurityType::Wpa,
1409 };
1410 let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec());
1411 let bssid_2 = types::Bssid::from([2, 2, 2, 2, 2, 2]);
1412
1413 assert!(
1415 exec.run_singlethreaded(
1416 test_values
1417 .real_saved_network_manager
1418 .store(test_id_1.clone(), credential_1.clone()),
1419 )
1420 .unwrap()
1421 .is_none()
1422 );
1423 assert!(
1424 exec.run_singlethreaded(
1425 test_values
1426 .real_saved_network_manager
1427 .store(test_id_2.clone(), credential_2.clone()),
1428 )
1429 .unwrap()
1430 .is_none()
1431 );
1432
1433 exec.run_singlethreaded(test_values.real_saved_network_manager.record_connect_result(
1435 test_id_1.clone(),
1436 &credential_1.clone(),
1437 bssid_1,
1438 fake_successful_connect_result(),
1439 types::ScanObservation::Passive,
1440 ));
1441 exec.run_singlethreaded(test_values.real_saved_network_manager.record_connect_result(
1442 test_id_2.clone(),
1443 &credential_2.clone(),
1444 bssid_2,
1445 fake_successful_connect_result(),
1446 types::ScanObservation::Passive,
1447 ));
1448
1449 let mutual_security_protocols_1 = [SecurityDescriptor::WPA3_PERSONAL];
1451 let channel_1 = generate_channel(3);
1452 let mutual_security_protocols_2 = [SecurityDescriptor::WPA1];
1453 let channel_2 = generate_channel(50);
1454 let mock_passive_scan_results = vec![
1455 types::ScanResult {
1456 ssid: test_id_1.ssid.clone(),
1457 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1458 compatibility: types::Compatibility::Supported,
1459 entries: vec![types::Bss {
1460 compatibility: wlan_common::scan::Compatible::expect_ok(
1461 mutual_security_protocols_1,
1462 ),
1463 bssid: bssid_1,
1464 channel: channel_1,
1465 signal: types::Signal {
1466 rssi_dbm: -30, snr_db: 0,
1468 },
1469 observation: types::ScanObservation::Passive,
1470 ..generate_random_bss()
1471 }],
1472 },
1473 types::ScanResult {
1474 ssid: test_id_2.ssid.clone(),
1475 security_type_detailed: types::SecurityTypeDetailed::Wpa1,
1476 compatibility: types::Compatibility::Supported,
1477 entries: vec![types::Bss {
1478 compatibility: wlan_common::scan::Compatible::expect_ok(
1479 mutual_security_protocols_2,
1480 ),
1481 bssid: bssid_2,
1482 channel: channel_2,
1483 signal: types::Signal {
1484 rssi_dbm: -100, snr_db: 0,
1486 },
1487 observation: types::ScanObservation::Passive,
1488 ..generate_random_bss()
1489 }],
1490 },
1491 generate_random_scan_result(),
1492 generate_random_scan_result(),
1493 ];
1494
1495 exec.run_singlethreaded(
1497 test_values.scan_requester.add_scan_result(Ok(mock_passive_scan_results.clone())),
1498 );
1499
1500 let bss_desc1_active = random_fidl_bss_description!();
1502 let new_bss = types::Bss {
1503 compatibility: wlan_common::scan::Compatible::expect_ok(mutual_security_protocols_1),
1504 bssid: bssid_1,
1505 bss_description: bss_desc1_active.clone().into(),
1506 ..generate_random_bss()
1507 };
1508 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1509 types::ScanResult {
1510 ssid: test_id_1.ssid.clone(),
1511 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1512 compatibility: types::Compatibility::Supported,
1513 entries: vec![new_bss.clone()],
1514 },
1515 generate_random_scan_result(),
1516 ])));
1517
1518 let mut connection_selection_fut = pin!(
1520 connection_selector
1521 .find_and_select_connection_candidate(None, generate_random_connect_reason())
1522 );
1523 let results =
1524 exec.run_singlethreaded(&mut connection_selection_fut).expect("no selected candidate");
1525 assert_eq!(&results.network, &test_id_1.clone());
1526 assert_eq!(
1527 &results.bss,
1528 &types::Bss {
1529 bss_description: bss_desc1_active.clone().into(),
1530 ..mock_passive_scan_results[0].entries[0].clone()
1531 }
1532 );
1533
1534 assert_eq!(
1536 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1537 vec![
1538 (ScanReason::NetworkSelection, vec![], vec![]),
1540 (
1542 ScanReason::BssSelectionAugmentation,
1543 vec![test_id_1.ssid.clone()],
1544 vec![channel_1]
1545 )
1546 ]
1547 );
1548
1549 assert_data_tree!(@executor exec, test_values.inspector, root: {
1551 connection_selection_test: {
1552 connection_selection: {
1553 "0": {
1554 "@time": AnyNumericProperty,
1555 "candidates": {
1556 "0": contains {
1557 bssid: &*BSSID_REGEX,
1558 score: AnyNumericProperty,
1559 },
1560 "1": contains {
1561 bssid: &*BSSID_REGEX,
1562 score: AnyNumericProperty,
1563 },
1564 },
1565 "selected": contains {
1566 bssid: &*BSSID_REGEX,
1567 score: AnyNumericProperty,
1568 },
1569 },
1570 }
1571 },
1572 });
1573
1574 assert_matches!(
1576 telemetry_receiver.try_next(),
1577 Ok(Some(TelemetryEvent::ActiveScanRequested { num_ssids_requested: 0 }))
1578 );
1579 assert_matches!(
1580 telemetry_receiver.try_next(), Ok(Some(TelemetryEvent::ConnectionSelectionScanResults {
1581 saved_network_count, bss_count_per_saved_network, saved_network_count_found_by_active_scan
1582 })) => {
1583 assert_eq!(saved_network_count, 2);
1584 assert_eq!(bss_count_per_saved_network, vec![1, 1]);
1585 assert_eq!(saved_network_count_found_by_active_scan, 0);
1586 }
1587 );
1588 assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
1589 assert_matches!(event, TelemetryEvent::NetworkSelectionDecision {
1590 network_selection_type: telemetry::NetworkSelectionType::Undirected,
1591 num_candidates: Ok(2),
1592 selected_count: 2,
1593 });
1594 });
1595 assert_matches!(
1596 telemetry_receiver.try_next(),
1597 Ok(Some(TelemetryEvent::BssSelectionResult { .. }))
1598 );
1599 }
1600
1601 #[fuchsia::test]
1602 fn find_and_select_connection_candidate_with_network_end_to_end() {
1603 let mut exec = fasync::TestExecutor::new();
1604 let test_values = exec.run_singlethreaded(test_setup(true));
1605 let connection_selector = test_values.connection_selector;
1606 let mut telemetry_receiver = test_values.telemetry_receiver;
1607
1608 let test_id_1 = types::NetworkIdentifier {
1610 ssid: types::Ssid::try_from("foo").unwrap(),
1611 security_type: types::SecurityType::Wpa3,
1612 };
1613
1614 assert!(
1616 exec.run_singlethreaded(
1617 test_values
1618 .real_saved_network_manager
1619 .store(test_id_1.clone(), TEST_PASSWORD.clone()),
1620 )
1621 .unwrap()
1622 .is_none()
1623 );
1624
1625 let mutual_security_protocols_1 = [SecurityDescriptor::WPA3_PERSONAL];
1627 let bss_desc_1 = random_fidl_bss_description!();
1628 let scanned_bss = types::Bss {
1629 compatibility: wlan_common::scan::Compatible::expect_ok(mutual_security_protocols_1),
1631 bss_description: bss_desc_1.clone().into(),
1632 observation: types::ScanObservation::Unknown,
1634 ..generate_random_bss()
1635 };
1636 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1637 types::ScanResult {
1638 ssid: test_id_1.ssid.clone(),
1639 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1641 compatibility: types::Compatibility::Supported,
1642 entries: vec![scanned_bss.clone()],
1643 },
1644 generate_random_scan_result(),
1645 generate_random_scan_result(),
1646 ])));
1647
1648 let connection_selection_fut = connection_selector.find_and_select_connection_candidate(
1650 Some(test_id_1.clone()),
1651 generate_random_connect_reason(),
1652 );
1653 let mut connection_selection_fut = pin!(connection_selection_fut);
1654 let results =
1655 exec.run_singlethreaded(&mut connection_selection_fut).expect("no selected candidate");
1656 assert_eq!(&results.network, &test_id_1);
1657 assert_eq!(&results.security_type_detailed, &types::SecurityTypeDetailed::Wpa3Personal);
1658 assert_eq!(&results.credential, &TEST_PASSWORD.clone());
1659 assert_eq!(&results.bss, &scanned_bss);
1660
1661 assert_eq!(
1663 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1664 vec![(ScanReason::BssSelection, vec![test_id_1.ssid.clone()], vec![])]
1665 );
1666 assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
1668 assert_matches!(event, TelemetryEvent::NetworkSelectionDecision {
1669 network_selection_type: telemetry::NetworkSelectionType::Directed,
1670 num_candidates: Ok(1),
1671 selected_count: 1,
1672 });
1673 });
1674 assert_matches!(
1675 telemetry_receiver.try_next(),
1676 Ok(Some(TelemetryEvent::BssSelectionResult { .. }))
1677 );
1678 }
1679
1680 #[fuchsia::test]
1681 fn find_and_select_connection_candidate_with_network_end_to_end_with_failure() {
1682 let mut exec = fasync::TestExecutor::new();
1683 let test_values = exec.run_singlethreaded(test_setup(true));
1684 let connection_selector = test_values.connection_selector;
1685 let mut telemetry_receiver = test_values.telemetry_receiver;
1686
1687 let test_id_1 = types::NetworkIdentifier {
1689 ssid: types::Ssid::try_from("foo").unwrap(),
1690 security_type: types::SecurityType::Wpa3,
1691 };
1692
1693 exec.run_singlethreaded(
1695 test_values.scan_requester.add_scan_result(Err(types::ScanError::GeneralError)),
1696 );
1697
1698 let connection_selection_fut = connection_selector.find_and_select_connection_candidate(
1700 Some(test_id_1.clone()),
1701 generate_random_connect_reason(),
1702 );
1703 let mut connection_selection_fut = pin!(connection_selection_fut);
1704
1705 let results = exec.run_singlethreaded(&mut connection_selection_fut);
1707 assert_eq!(results, None);
1708
1709 assert_eq!(
1711 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1712 vec![(ScanReason::BssSelection, vec![test_id_1.ssid.clone()], vec![])]
1713 );
1714
1715 assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
1717 assert_matches!(event, TelemetryEvent::NetworkSelectionDecision {
1718 network_selection_type: telemetry::NetworkSelectionType::Directed,
1719 num_candidates: Err(()),
1720 selected_count: 1,
1721 });
1722 });
1723
1724 assert_matches!(
1725 telemetry_receiver.try_next(),
1726 Ok(Some(TelemetryEvent::BssSelectionResult { .. }))
1727 );
1728 }
1729
1730 #[test_case(fidl_common::ScanType::Active)]
1731 #[test_case(fidl_common::ScanType::Passive)]
1732 #[fuchsia::test(add_test_attr = false)]
1733 fn find_and_select_roam_candidate_requests_typed_scan(scan_type: fidl_common::ScanType) {
1734 let mut exec = fasync::TestExecutor::new();
1735 let test_values = exec.run_singlethreaded(test_setup(true));
1736 let connection_selector = test_values.connection_selector;
1737
1738 let test_id = types::NetworkIdentifier {
1739 ssid: types::Ssid::try_from("foo").unwrap(),
1740 security_type: types::SecurityType::Wpa3,
1741 };
1742 let credential = generate_random_password();
1743
1744 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1746 types::ScanResult {
1747 ssid: test_id.ssid.clone(),
1748 security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1749 compatibility: types::Compatibility::Supported,
1750 entries: vec![],
1751 },
1752 ])));
1753
1754 let roam_selection_fut = connection_selector.find_and_select_roam_candidate(
1756 scan_type,
1757 test_id.clone(),
1758 &credential,
1759 types::SecurityTypeDetailed::Wpa3Personal,
1760 );
1761 let mut roam_selection_fut = pin!(roam_selection_fut);
1762 let _ = exec.run_singlethreaded(&mut roam_selection_fut);
1763
1764 if scan_type == fidl_common::ScanType::Active {
1766 assert_eq!(
1767 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1768 vec![(ScanReason::RoamSearch, vec![test_id.ssid.clone()], vec![])]
1769 );
1770 } else {
1771 assert_eq!(
1772 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1773 vec![(ScanReason::RoamSearch, vec![], vec![])]
1774 );
1775 }
1776 }
1777
1778 #[fuchsia::test]
1779 fn find_and_select_roam_candidate_active_scans_if_no_results_found() {
1780 let mut exec = fasync::TestExecutor::new();
1781 let test_values = exec.run_singlethreaded(test_setup(true));
1782 let connection_selector = test_values.connection_selector;
1783
1784 let test_id = generate_random_network_identifier();
1785 let credential = generate_random_password();
1786
1787 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![])));
1789 exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![])));
1790
1791 let roam_selection_fut = connection_selector.find_and_select_roam_candidate(
1793 fidl_common::ScanType::Passive,
1794 test_id.clone(),
1795 &credential,
1796 types::SecurityTypeDetailed::Open,
1797 );
1798
1799 let mut roam_selection_fut = pin!(roam_selection_fut);
1800 let _ = exec.run_singlethreaded(&mut roam_selection_fut);
1801
1802 assert_eq!(
1805 *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1806 vec![
1807 (ScanReason::RoamSearch, vec![], vec![]),
1808 (ScanReason::RoamSearch, vec![test_id.ssid.clone()], vec![])
1809 ]
1810 );
1811 }
1812
1813 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
1814 #[fuchsia::test]
1815 async fn recorded_metrics_on_scan() {
1816 let (telemetry_sender, mut telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1817 let telemetry_sender = TelemetrySender::new(telemetry_sender);
1818
1819 let test_id_1 = types::NetworkIdentifier {
1821 ssid: types::Ssid::try_from("foo").unwrap().clone(),
1822 security_type: types::SecurityType::Wpa3,
1823 };
1824
1825 let test_id_2 = types::NetworkIdentifier {
1826 ssid: types::Ssid::try_from("bar").unwrap().clone(),
1827 security_type: types::SecurityType::Wpa,
1828 };
1829
1830 let mut mock_scan_results = vec![];
1831
1832 mock_scan_results.push(types::ScannedCandidate {
1833 network: test_id_1.clone(),
1834 bss: types::Bss {
1835 observation: types::ScanObservation::Passive,
1836 ..generate_random_bss()
1837 },
1838 ..generate_random_scanned_candidate()
1839 });
1840 mock_scan_results.push(types::ScannedCandidate {
1841 network: test_id_1.clone(),
1842 bss: types::Bss {
1843 observation: types::ScanObservation::Passive,
1844 ..generate_random_bss()
1845 },
1846 ..generate_random_scanned_candidate()
1847 });
1848 mock_scan_results.push(types::ScannedCandidate {
1849 network: test_id_1.clone(),
1850 bss: types::Bss {
1851 observation: types::ScanObservation::Active,
1852 ..generate_random_bss()
1853 },
1854 ..generate_random_scanned_candidate()
1855 });
1856 mock_scan_results.push(types::ScannedCandidate {
1857 network: test_id_2.clone(),
1858 bss: types::Bss {
1859 observation: types::ScanObservation::Passive,
1860 ..generate_random_bss()
1861 },
1862 ..generate_random_scanned_candidate()
1863 });
1864
1865 record_metrics_on_scan(mock_scan_results, &telemetry_sender);
1866
1867 assert_matches!(
1868 telemetry_receiver.try_next(), Ok(Some(TelemetryEvent::ConnectionSelectionScanResults {
1869 saved_network_count, mut bss_count_per_saved_network, saved_network_count_found_by_active_scan
1870 })) => {
1871 assert_eq!(saved_network_count, 2);
1872 bss_count_per_saved_network.sort();
1873 assert_eq!(bss_count_per_saved_network, vec![1, 3]);
1874 assert_eq!(saved_network_count_found_by_active_scan, 1);
1875 }
1876 );
1877 }
1878
1879 #[fuchsia::test]
1880 async fn recorded_metrics_on_scan_no_saved_networks() {
1881 let (telemetry_sender, mut telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1882 let telemetry_sender = TelemetrySender::new(telemetry_sender);
1883
1884 let mock_scan_results = vec![];
1885
1886 record_metrics_on_scan(mock_scan_results, &telemetry_sender);
1887
1888 assert_matches!(
1889 telemetry_receiver.try_next(), Ok(Some(TelemetryEvent::ConnectionSelectionScanResults {
1890 saved_network_count, bss_count_per_saved_network, saved_network_count_found_by_active_scan
1891 })) => {
1892 assert_eq!(saved_network_count, 0);
1893 assert_eq!(bss_count_per_saved_network, Vec::<usize>::new());
1894 assert_eq!(saved_network_count_found_by_active_scan, 0);
1895 }
1896 );
1897 }
1898
1899 struct FakeConnectionSelector {
1902 pub response_to_find_and_select_connection_candidate: Option<types::ScannedCandidate>,
1903 pub response_to_find_and_select_roam_candidate: Option<types::ScannedCandidate>,
1904 }
1905 #[async_trait(?Send)]
1906 impl ConnectionSelectorApi for FakeConnectionSelector {
1907 async fn find_and_select_connection_candidate(
1908 &self,
1909 _network: Option<types::NetworkIdentifier>,
1910 _reason: types::ConnectReason,
1911 ) -> Option<types::ScannedCandidate> {
1912 self.response_to_find_and_select_connection_candidate.clone()
1913 }
1914 async fn find_and_select_roam_candidate(
1915 &self,
1916 _scan_type: fidl_common::ScanType,
1917 _network: types::NetworkIdentifier,
1918 _credential: &network_config::Credential,
1919 _scanned_securioty_type: types::SecurityTypeDetailed,
1920 ) -> Option<types::ScannedCandidate> {
1921 self.response_to_find_and_select_roam_candidate.clone()
1922 }
1923 }
1924 #[fuchsia::test]
1925 fn test_service_loop_passes_connection_selection_request() {
1926 let mut exec = fasync::TestExecutor::new();
1927
1928 let candidate = generate_random_scanned_candidate();
1930 let connection_selector = Rc::new(FakeConnectionSelector {
1931 response_to_find_and_select_connection_candidate: Some(candidate.clone()),
1932 response_to_find_and_select_roam_candidate: None,
1933 });
1934
1935 let (request_sender, request_receiver) = mpsc::channel(5);
1937 let mut serve_fut =
1938 pin!(serve_connection_selection_request_loop(connection_selector, request_receiver));
1939 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
1940
1941 let mut requester = ConnectionSelectionRequester { sender: request_sender };
1943
1944 let mut connection_selection_fut = pin!(
1946 requester.do_connection_selection(None, types::ConnectReason::IdleInterfaceAutoconnect)
1947 );
1948 assert_matches!(exec.run_until_stalled(&mut connection_selection_fut), Poll::Pending);
1949
1950 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
1952
1953 assert_matches!(exec.run_until_stalled(&mut connection_selection_fut), Poll::Ready(Ok(Some(selected_candidate))) => {
1955 assert_eq!(selected_candidate, candidate);
1956 });
1957 }
1958
1959 #[fuchsia::test]
1960 fn test_service_loop_passes_roam_selection_request() {
1961 let mut exec = fasync::TestExecutor::new();
1962
1963 let candidate = generate_random_scanned_candidate();
1965 let connection_selector = Rc::new(FakeConnectionSelector {
1966 response_to_find_and_select_connection_candidate: None,
1967 response_to_find_and_select_roam_candidate: Some(candidate.clone()),
1968 });
1969
1970 let (request_sender, request_receiver) = mpsc::channel(5);
1972 let mut serve_fut =
1973 pin!(serve_connection_selection_request_loop(connection_selector, request_receiver));
1974 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
1975
1976 let mut requester = ConnectionSelectionRequester { sender: request_sender };
1978
1979 let bss_desc =
1981 BssDescription::try_from(Sequestered::release(candidate.bss.bss_description.clone()))
1982 .unwrap();
1983 let mut roam_selection_fut = pin!(requester.do_roam_selection(
1984 fidl_common::ScanType::Passive,
1985 candidate.network.clone(),
1986 candidate.credential.clone(),
1987 bss_desc.protection().into()
1988 ));
1989 assert_matches!(exec.run_until_stalled(&mut roam_selection_fut), Poll::Pending);
1990
1991 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
1993
1994 assert_matches!(exec.run_until_stalled(&mut roam_selection_fut), Poll::Ready(Ok(Some(selected_candidate))) => {
1996 assert_eq!(selected_candidate, candidate);
1997 });
1998 }
1999}