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 .filter(|c| &c.credential == credential)
462 {
463 candidates.append(&mut merge_config_and_scan_data(config, &mut s));
464 } else {
465 warn!("Failed to find config for network to roam from");
468 }
469 }
470 bss_selection::select_bss(
472 candidates,
473 types::ConnectReason::ProactiveNetworkSwitch,
474 self.inspect_node_for_connection_selection.clone(),
475 self.telemetry_sender.clone(),
476 )
477 .await
478 }
479}
480
481impl types::ScannedCandidate {
482 pub fn recent_failure_count(&self) -> u64 {
483 self.saved_network_info
484 .recent_failures
485 .iter()
486 .filter(|failure| failure.bssid == self.bss.bssid)
487 .count()
488 .try_into()
489 .unwrap_or_else(|e| {
490 error!("{}", e);
491 u64::MAX
492 })
493 }
494 pub fn recent_short_connections(&self) -> usize {
495 self.saved_network_info
496 .past_connections
497 .get_list_for_bss(&self.bss.bssid)
498 .get_recent(fasync::MonotonicInstant::now() - RECENT_DISCONNECT_WINDOW)
499 .iter()
500 .filter(|d| d.connection_uptime < SHORT_CONNECT_DURATION)
501 .count()
502 }
503
504 pub fn saved_security_type_to_string(&self) -> String {
505 match self.network.security_type {
506 SecurityType::None => "open",
507 SecurityType::Wep => "WEP",
508 SecurityType::Wpa => "WPA1",
509 SecurityType::Wpa2 => "WPA2",
510 SecurityType::Wpa3 => "WPA3",
511 }
512 .to_string()
513 }
514
515 pub fn scanned_security_type_to_string(&self) -> String {
516 match self.security_type_detailed {
517 SecurityTypeDetailed::Unknown => "unknown",
518 SecurityTypeDetailed::Open => "open",
519 SecurityTypeDetailed::OpenOweTransition => "Open OWE Transition",
520 SecurityTypeDetailed::Owe => "OWE",
521 SecurityTypeDetailed::Wep => "WEP",
522 SecurityTypeDetailed::Wpa1 => "WPA1",
523 SecurityTypeDetailed::Wpa1Wpa2PersonalTkipOnly => "WPA1/2Tk",
524 SecurityTypeDetailed::Wpa2PersonalTkipOnly => "WPA2Tk",
525 SecurityTypeDetailed::Wpa1Wpa2Personal => "WPA1/2",
526 SecurityTypeDetailed::Wpa2Personal => "WPA2",
527 SecurityTypeDetailed::Wpa2Wpa3Personal => "WPA2/3",
528 SecurityTypeDetailed::Wpa3Personal => "WPA3",
529 SecurityTypeDetailed::Wpa2Enterprise => "WPA2Ent",
530 SecurityTypeDetailed::Wpa3Enterprise => "WPA3Ent",
531 }
532 .to_string()
533 }
534
535 pub fn to_string_without_pii(&self) -> String {
536 let channel = self.bss.channel;
537 let rssi = self.bss.signal.rssi_dbm;
538 let recent_failure_count = self.recent_failure_count();
539 let recent_short_connection_count = self.recent_short_connections();
540
541 format!(
542 "{}({:4}), {}({:6}), {:>4}dBm, channel {:8}, score {:4}{}{}{}{}",
543 self.network.ssid,
544 self.saved_security_type_to_string(),
545 self.bss.bssid,
546 self.scanned_security_type_to_string(),
547 rssi,
548 channel,
549 scoring_functions::score_bss_scanned_candidate(self.clone()),
550 if !self.bss.is_compatible() { ", NOT compatible" } else { "" },
551 if recent_failure_count > 0 {
552 format!(", {recent_failure_count} recent failures")
553 } else {
554 "".to_string()
555 },
556 if recent_short_connection_count > 0 {
557 format!(", {recent_short_connection_count} recent short disconnects")
558 } else {
559 "".to_string()
560 },
561 if !self.saved_network_info.has_ever_connected { ", never used yet" } else { "" },
562 )
563 }
564}
565
566impl WriteInspect for types::ScannedCandidate {
567 fn write_inspect<'a>(&self, writer: &InspectNode, key: impl Into<Cow<'a, str>>) {
568 inspect_insert!(writer, var key: {
569 ssid: self.network.ssid.to_string(),
570 bssid: self.bss.bssid.to_string(),
571 rssi: self.bss.signal.rssi_dbm,
572 score: scoring_functions::score_bss_scanned_candidate(self.clone()),
573 security_type_saved: self.saved_security_type_to_string(),
574 security_type_scanned: format!(
575 "{}",
576 wlan_common::bss::Protection::from(self.security_type_detailed),
577 ),
578 channel: format!("{}", self.bss.channel),
579 compatible: self.bss.is_compatible(),
580 incompatibility: self.bss
581 .compatibility
582 .as_ref()
583 .err()
584 .map(ToString::to_string)
585 .unwrap_or_else(|| String::from("none")),
586 recent_failure_count: self.recent_failure_count(),
587 saved_network_has_ever_connected: self.saved_network_info.has_ever_connected,
588 });
589 }
590}
591
592fn get_authenticator(bss: &Bss, credential: &Credential) -> Option<SecurityAuthenticator> {
593 let mutual_security_protocols = match bss.compatibility.as_ref() {
594 Ok(compatible) => compatible.mutual_security_protocols().clone(),
595 Err(incompatible) => {
596 error!("BSS ({:?}) is incompatible: {}", bss.bssid, incompatible);
597 return None;
598 }
599 };
600
601 match select_authentication_method(mutual_security_protocols.clone(), credential) {
602 Some(authenticator) => Some(authenticator),
603 None => {
604 error!(
605 "Failed to negotiate authentication for BSS ({:?}) with mutually supported \
606 security protocols: {:?}, and credential type: {:?}.",
607 bss.bssid,
608 mutual_security_protocols,
609 credential.type_str()
610 );
611 None
612 }
613 }
614}
615
616fn merge_config_and_scan_data(
617 network_config: config_management::NetworkConfig,
618 scan_result: &mut types::ScanResult,
619) -> Vec<types::ScannedCandidate> {
620 if network_config.ssid != scan_result.ssid
621 || !network_config
622 .security_type
623 .is_compatible_with_scanned_type(&scan_result.security_type_detailed)
624 {
625 return Vec::new();
626 }
627
628 let mut merged_networks = Vec::new();
629 let multiple_bss_candidates = scan_result.entries.len() > 1;
630
631 for bss in scan_result.entries.iter() {
632 let authenticator = match get_authenticator(bss, &network_config.credential) {
633 Some(authenticator) => authenticator,
634 None => {
635 error!(
636 "Failed to create authenticator for bss candidate {:?} (SSID: {:?}). Removing from candidates.",
637 bss.bssid, network_config.ssid
638 );
639 continue;
640 }
641 };
642 let scanned_candidate = types::ScannedCandidate {
643 network: types::NetworkIdentifier {
644 ssid: network_config.ssid.clone(),
645 security_type: network_config.security_type,
646 },
647 security_type_detailed: scan_result.security_type_detailed,
648 credential: network_config.credential.clone(),
649 network_has_multiple_bss: multiple_bss_candidates,
650 saved_network_info: InternalSavedNetworkData {
651 has_ever_connected: network_config.has_ever_connected,
652 recent_failures: network_config.perf_stats.connect_failures.get_recent_for_network(
653 fasync::MonotonicInstant::now() - RECENT_FAILURE_WINDOW,
654 ),
655 past_connections: network_config.perf_stats.past_connections.clone(),
656 },
657 bss: bss.clone(),
658 authenticator,
659 };
660 merged_networks.push(scanned_candidate)
661 }
662 merged_networks
663}
664
665async fn merge_saved_networks_and_scan_data(
668 saved_network_manager: &Arc<dyn SavedNetworksManagerApi>,
669 mut scan_results: Vec<types::ScanResult>,
670) -> Vec<types::ScannedCandidate> {
671 let mut merged_networks = vec![];
672 for mut scan_result in scan_results.drain(..) {
673 for saved_config in saved_network_manager
674 .lookup_compatible(&scan_result.ssid, scan_result.security_type_detailed)
675 .await
676 {
677 let multiple_bss_candidates = scan_result.entries.len() > 1;
678 for bss in scan_result.entries.drain(..) {
679 let authenticator = match get_authenticator(&bss, &saved_config.credential) {
680 Some(authenticator) => authenticator,
681 None => {
682 error!(
683 "Failed to create authenticator for bss candidate {} (SSID: {}). Removing from candidates.",
684 bss.bssid, saved_config.ssid
685 );
686 continue;
687 }
688 };
689 let scanned_candidate = types::ScannedCandidate {
690 network: types::NetworkIdentifier {
691 ssid: saved_config.ssid.clone(),
692 security_type: saved_config.security_type,
693 },
694 security_type_detailed: scan_result.security_type_detailed,
695 credential: saved_config.credential.clone(),
696 network_has_multiple_bss: multiple_bss_candidates,
697 saved_network_info: InternalSavedNetworkData {
698 has_ever_connected: saved_config.has_ever_connected,
699 recent_failures: saved_config
700 .perf_stats
701 .connect_failures
702 .get_recent_for_network(
703 fasync::MonotonicInstant::now() - RECENT_FAILURE_WINDOW,
704 ),
705 past_connections: saved_config.perf_stats.past_connections.clone(),
706 },
707 bss,
708 authenticator,
709 };
710 merged_networks.push(scanned_candidate)
711 }
712 }
713 }
714 merged_networks
715}
716
717fn record_metrics_on_scan(
718 mut merged_networks: Vec<types::ScannedCandidate>,
719 telemetry_sender: &TelemetrySender,
720) {
721 let mut merged_network_map: HashMap<types::NetworkIdentifier, Vec<types::ScannedCandidate>> =
722 HashMap::new();
723 for bss in merged_networks.drain(..) {
724 merged_network_map.entry(bss.network.clone()).or_default().push(bss);
725 }
726
727 let num_saved_networks_observed = merged_network_map.len();
728 let mut num_actively_scanned_networks = 0;
729 let bss_count_per_saved_network = merged_network_map
730 .values()
731 .map(|bsss| {
732 if bsss.iter().any(|bss| matches!(bss.bss.observation, types::ScanObservation::Active))
734 {
735 num_actively_scanned_networks += 1;
736 };
737 bsss.len()
739 })
740 .collect();
741
742 telemetry_sender.send(TelemetryEvent::ConnectionSelectionScanResults {
743 saved_network_count: num_saved_networks_observed,
744 bss_count_per_saved_network,
745 saved_network_count_found_by_active_scan: num_actively_scanned_networks,
746 });
747}
748#[cfg(test)]
749mod tests {
750 use super::*;
751 use crate::config_management::network_config::HistoricalListsByBssid;
752 use crate::config_management::{ConnectFailure, FailureReason, SavedNetworksManager};
753 use crate::util::testing::fakes::{FakeSavedNetworksManager, FakeScanRequester};
754 use crate::util::testing::{
755 generate_channel, generate_random_bss, generate_random_connect_reason,
756 generate_random_network_identifier, generate_random_password, generate_random_scan_result,
757 generate_random_scanned_candidate,
758 };
759 use assert_matches::assert_matches;
760 use diagnostics_assertions::{AnyNumericProperty, assert_data_tree};
761 use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
762 use fidl_fuchsia_wlan_sme as fidl_sme;
763 use fuchsia_async as fasync;
764 use fuchsia_inspect as inspect;
765 use futures::task::Poll;
766 use ieee80211_testutils::BSSID_REGEX;
767 use rand::Rng;
768 use std::pin::pin;
769 use std::rc::Rc;
770 use std::sync::LazyLock;
771 use test_case::test_case;
772 use wlan_common::bss::BssDescription;
773 use wlan_common::random_fidl_bss_description;
774 use wlan_common::scan::Compatible;
775 use wlan_common::security::SecurityDescriptor;
776
777 pub static TEST_PASSWORD: LazyLock<Credential> =
778 LazyLock::new(|| Credential::Password(b"password".to_vec()));
779
780 struct TestValues {
781 connection_selector: Rc<ConnectionSelector>,
782 real_saved_network_manager: Arc<dyn SavedNetworksManagerApi>,
783 saved_network_manager: Arc<FakeSavedNetworksManager>,
784 scan_requester: Arc<FakeScanRequester>,
785 inspector: inspect::Inspector,
786 telemetry_receiver: mpsc::Receiver<TelemetryEvent>,
787 }
788
789 async fn test_setup(use_real_save_network_manager: bool) -> TestValues {
790 let real_saved_network_manager = Arc::new(SavedNetworksManager::new_for_test().await);
791 let saved_network_manager = Arc::new(FakeSavedNetworksManager::new());
792 let scan_requester = Arc::new(FakeScanRequester::new());
793 let inspector = inspect::Inspector::default();
794 let inspect_node = inspector.root().create_child("connection_selection_test");
795 let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
796
797 let connection_selector = Rc::new(ConnectionSelector::new(
798 if use_real_save_network_manager {
799 real_saved_network_manager.clone()
800 } else {
801 saved_network_manager.clone()
802 },
803 scan_requester.clone(),
804 inspect_node,
805 TelemetrySender::new(telemetry_sender),
806 ));
807
808 TestValues {
809 connection_selector,
810 real_saved_network_manager,
811 saved_network_manager,
812 scan_requester,
813 inspector,
814 telemetry_receiver,
815 }
816 }
817
818 fn fake_successful_connect_result() -> fidl_sme::ConnectResult {
819 fidl_sme::ConnectResult {
820 code: fidl_ieee80211::StatusCode::Success,
821 is_credential_rejected: false,
822 is_reconnect: false,
823 }
824 }
825
826 #[fuchsia::test]
827 async fn scan_results_merged_with_saved_networks() {
828 let test_values = test_setup(true).await;
829
830 let test_ssid_1 = types::Ssid::try_from("foo").unwrap();
832 let test_security_1 = types::SecurityTypeDetailed::Wpa3Personal;
833 let test_id_1 = types::NetworkIdentifier {
834 ssid: test_ssid_1.clone(),
835 security_type: types::SecurityType::Wpa3,
836 };
837 let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
838 let test_ssid_2 = types::Ssid::try_from("bar").unwrap();
839 let test_security_2 = types::SecurityTypeDetailed::Wpa1;
840 let test_id_2 = types::NetworkIdentifier {
841 ssid: test_ssid_2.clone(),
842 security_type: types::SecurityType::Wpa,
843 };
844 let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec());
845
846 assert!(
848 test_values
849 .real_saved_network_manager
850 .store(test_id_1.clone(), credential_1.clone())
851 .await
852 .unwrap()
853 .is_none()
854 );
855
856 assert!(
857 test_values
858 .real_saved_network_manager
859 .store(test_id_2.clone(), credential_2.clone())
860 .await
861 .unwrap()
862 .is_none()
863 );
864
865 let mock_scan_results = vec![
867 types::ScanResult {
868 ssid: test_ssid_1.clone(),
869 security_type_detailed: test_security_1,
870 entries: vec![
871 types::Bss {
872 compatibility: Compatible::expect_ok([SecurityDescriptor::WPA3_PERSONAL]),
873 ..generate_random_bss()
874 },
875 types::Bss {
876 compatibility: Compatible::expect_ok([SecurityDescriptor::WPA3_PERSONAL]),
877 ..generate_random_bss()
878 },
879 types::Bss {
880 compatibility: Compatible::expect_ok([SecurityDescriptor::WPA3_PERSONAL]),
881 ..generate_random_bss()
882 },
883 ],
884 compatibility: types::Compatibility::Supported,
885 },
886 types::ScanResult {
887 ssid: test_ssid_2.clone(),
888 security_type_detailed: test_security_2,
889 entries: vec![types::Bss {
890 compatibility: Compatible::expect_ok([SecurityDescriptor::WPA1]),
891 ..generate_random_bss()
892 }],
893 compatibility: types::Compatibility::DisallowedNotSupported,
894 },
895 ];
896
897 let bssid_1 = mock_scan_results[0].entries[0].bssid;
898 let bssid_2 = mock_scan_results[0].entries[1].bssid;
899
900 test_values
902 .real_saved_network_manager
903 .record_connect_result(
904 test_id_1.clone(),
905 &credential_1.clone(),
906 bssid_1,
907 fake_successful_connect_result(),
908 types::ScanObservation::Unknown,
909 )
910 .await;
911
912 test_values
914 .real_saved_network_manager
915 .record_connect_result(
916 test_id_1.clone(),
917 &credential_1.clone(),
918 bssid_2,
919 fidl_sme::ConnectResult {
920 code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
921 is_credential_rejected: true,
922 ..fake_successful_connect_result()
923 },
924 types::ScanObservation::Unknown,
925 )
926 .await;
927
928 let failure_time = test_values
930 .real_saved_network_manager
931 .lookup(&test_id_1.clone())
932 .await
933 .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, fidl_fuchsia_wlan_ieee80211::WlanBand::TwoGhz);
1451 let mutual_security_protocols_2 = [SecurityDescriptor::WPA1];
1452 let channel_2 = generate_channel(50, fidl_fuchsia_wlan_ieee80211::WlanBand::FiveGhz);
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}