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