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