1use crate::client::config_management::Credential;
6use crate::client::connection_selection::ConnectionSelectionRequester;
7use crate::client::roaming::lib::*;
8use crate::client::types;
9use crate::telemetry::{TelemetryEvent, TelemetrySender};
10use anyhow::{Error, format_err};
11use async_trait::async_trait;
12use fidl_fuchsia_wlan_common as fidl_common;
13use fidl_fuchsia_wlan_internal as fidl_internal;
14use futures::channel::mpsc;
15use futures::future::LocalBoxFuture;
16use futures::lock::Mutex;
17use futures::stream::{FuturesUnordered, StreamExt};
18use futures::{FutureExt, select};
19use log::{debug, error, info, warn};
20use std::any::Any;
21use std::sync::Arc;
22
23pub mod default_monitor;
24pub mod stationary_monitor;
25
26pub struct RoamDataSender {
28 sender: mpsc::Sender<RoamTriggerData>,
29}
30impl RoamDataSender {
31 pub fn new(trigger_data_sender: mpsc::Sender<RoamTriggerData>) -> Self {
32 Self { sender: trigger_data_sender }
33 }
34 pub fn send_signal_report_ind(
35 &mut self,
36 ind: fidl_internal::SignalReportIndication,
37 ) -> Result<(), anyhow::Error> {
38 Ok(self.sender.try_send(RoamTriggerData::SignalReportInd(ind))?)
39 }
40}
41#[async_trait(?Send)]
43pub trait RoamMonitorApi: Any {
44 async fn handle_roam_trigger_data(
48 &mut self,
49 data: RoamTriggerData,
50 ) -> Result<RoamTriggerDataOutcome, anyhow::Error>;
51 fn should_send_roam_request(&self, request: PolicyRoamRequest) -> Result<bool, anyhow::Error>;
54 fn notify_of_roam_attempt(&mut self);
57}
58
59pub async fn serve_roam_monitor(
62 mut roam_monitor: Box<dyn RoamMonitorApi>,
63 roaming_policy: RoamingPolicy,
64 mut trigger_data_receiver: mpsc::Receiver<RoamTriggerData>,
65 connection_selection_requester: ConnectionSelectionRequester,
66 mut roam_request_sender: mpsc::Sender<PolicyRoamRequest>,
67 telemetry_sender: TelemetrySender,
68 past_roams: Arc<Mutex<PastRoamList>>,
69) -> Result<(), anyhow::Error> {
70 let mut roam_search_result_futs: FuturesUnordered<
72 LocalBoxFuture<'static, Result<PolicyRoamRequest, Error>>,
73 > = FuturesUnordered::new();
74
75 loop {
76 select! {
77 trigger_data = trigger_data_receiver.next() => if let Some(data) = trigger_data {
79 match roam_monitor.handle_roam_trigger_data(data).await {
80 Ok(RoamTriggerDataOutcome::RoamSearch { scan_type, network_identifier, credential, current_security, reasons}) => {
81 telemetry_sender.send(TelemetryEvent::PolicyRoamScan { reasons: reasons.clone() });
82 info!("Performing scan to find proactive local roaming candidates.");
83 let roam_search_fut = get_roaming_connection_selection_future(
84 connection_selection_requester.clone(),
85 scan_type,
86 network_identifier,
87 credential,
88 current_security,
89 reasons
90 );
91 roam_search_result_futs.push(roam_search_fut.boxed());
92 },
93 Ok(RoamTriggerDataOutcome::Noop) => {},
94 Err(e) => error!("error handling roam trigger data: {}", e),
95 }
96 },
97 roam_search_result = roam_search_result_futs.select_next_some() => match roam_search_result {
100 Ok(request) => {
101 if roam_monitor.should_send_roam_request(request.clone()).unwrap_or_else(|e| {
102 error!("Error validating selected roam candidate: {}", e);
103 false
104 }) {
105 match roaming_policy {
106 RoamingPolicy::Enabled { mode: RoamingMode::CanRoam, ..} => {
107 info!("Requesting roam to candidate: {:?}", request.candidate.to_string_without_pii());
108 if roam_request_sender.try_send(request).is_err() {
109 warn!("Failed to send roam request, exiting monitor service loop.");
110 break
111 }
112 }
113 _ => {
114 debug!("Roaming policy is {:?}. Skipping roam request.", roaming_policy);
115 telemetry_sender.send(TelemetryEvent::WouldRoamConnect)
116 }
117 }
118 if let Some(mut past_roams) = past_roams.try_lock() {
120 past_roams.add(RoamEvent::new_roam_now());
121 } else {
122 error!("Unexpectedly failed to acquire lock on past roam list; will not record roam");
123 }
124 }
125 }
126 Err(e) => {
127 error!("Error occured during roam search: {:?}", e);
128 }
129 },
130 complete => {
131 debug!("Roam monitor channels dropped, exiting monitor service loop.");
132 break
133 }
134 }
135 }
136 Ok(())
137}
138
139async fn get_roaming_connection_selection_future(
142 mut connection_selection_requester: ConnectionSelectionRequester,
143 scan_type: fidl_common::ScanType,
144 network_identifier: types::NetworkIdentifier,
145 credential: Credential,
146 current_security: types::SecurityTypeDetailed,
147 reasons: Vec<RoamReason>,
148) -> Result<PolicyRoamRequest, Error> {
149 match connection_selection_requester
150 .do_roam_selection(scan_type, network_identifier, credential, current_security)
151 .await?
152 {
153 Some(candidate) => Ok(PolicyRoamRequest { candidate, reasons }),
154 None => Err(format_err!("No roam candidates found.")),
155 }
156}
157
158#[cfg(test)]
159mod test {
160 use super::*;
161 use crate::client::connection_selection::ConnectionSelectionRequest;
162 use crate::client::roaming::lib::RoamingProfile;
163 use crate::telemetry::TelemetryEvent;
164 use crate::util::testing::fakes::FakeRoamMonitor;
165 use crate::util::testing::{
166 generate_random_network_identifier, generate_random_password,
167 generate_random_scanned_candidate,
168 };
169 use assert_matches::assert_matches;
170 use fidl_fuchsia_wlan_common as fidl_common;
171 use fidl_fuchsia_wlan_internal as fidl_internal;
172 use fuchsia_async::{self as fasync, TestExecutor};
173 use futures::task::Poll;
174 use futures::{Future, pin_mut};
175 use std::pin::Pin;
176 use test_case::test_case;
177
178 struct TestValues {
179 trigger_data_sender: mpsc::Sender<RoamTriggerData>,
180 trigger_data_receiver: mpsc::Receiver<RoamTriggerData>,
181 roam_request_sender: mpsc::Sender<PolicyRoamRequest>,
182 roam_request_receiver: mpsc::Receiver<PolicyRoamRequest>,
183 connection_selection_requester: ConnectionSelectionRequester,
184 connection_selection_request_receiver: mpsc::Receiver<ConnectionSelectionRequest>,
185 telemetry_sender: TelemetrySender,
186 telemetry_receiver: mpsc::Receiver<TelemetryEvent>,
187 past_roams: Arc<Mutex<PastRoamList>>,
188 }
189
190 fn setup_test() -> TestValues {
191 let (trigger_data_sender, trigger_data_receiver) = mpsc::channel(100);
192 let (roam_sender, roam_receiver) = mpsc::channel(100);
193 let (connection_selection_request_sender, connection_selection_request_receiver) =
194 mpsc::channel(5);
195 let connection_selection_requester =
196 ConnectionSelectionRequester::new(connection_selection_request_sender);
197 let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
198 let telemetry_sender = TelemetrySender::new(telemetry_sender);
199 let past_roams = Arc::new(Mutex::new(PastRoamList::new()));
200 TestValues {
201 trigger_data_sender,
202 trigger_data_receiver,
203 roam_request_sender: roam_sender,
204 roam_request_receiver: roam_receiver,
205 connection_selection_requester,
206 connection_selection_request_receiver,
207 telemetry_sender,
208 telemetry_receiver,
209 past_roams,
210 }
211 }
212
213 #[fuchsia::test]
214 fn test_roam_data_sender_send_signal_report_ind() {
215 let _exec = TestExecutor::new();
216 let (sender, mut receiver) = mpsc::channel(100);
217 let mut roam_data_sender = RoamDataSender::new(sender);
218 let ind = fidl_internal::SignalReportIndication {
219 rssi_dbm: Some(-60),
220 snr_db: Some(30),
221 tx_rate_500kbps: Some(0),
222 ..Default::default()
223 };
224
225 roam_data_sender.send_signal_report_ind(ind.clone()).expect("error sending signal report");
226
227 assert_matches!(receiver.try_recv(), Ok(RoamTriggerData::SignalReportInd(data)) => {
229 assert_eq!(ind, data);
230 });
231 }
232
233 #[test_case(RoamTriggerDataOutcome::Noop; "should not queue roam search")]
234 #[test_case(RoamTriggerDataOutcome::RoamSearch { scan_type: fidl_common::ScanType::Passive, network_identifier: generate_random_network_identifier(), credential: generate_random_password(), current_security: types::SecurityTypeDetailed::Open, reasons: vec![]}; "should queue roam search")]
235 #[fuchsia::test(add_test_attr = false)]
236 fn test_serve_loop_handles_trigger_data(response_to_should_roam_scan: RoamTriggerDataOutcome) {
237 let mut exec = TestExecutor::new();
238 let mut test_values = setup_test();
239
240 let mut roam_monitor = FakeRoamMonitor::new();
243 roam_monitor.response_to_should_roam_scan = response_to_should_roam_scan.clone();
244
245 let serve_fut = serve_roam_monitor(
247 Box::new(roam_monitor),
248 RoamingPolicy::Enabled {
249 profile: RoamingProfile::Stationary,
250 mode: RoamingMode::CanRoam,
251 },
252 test_values.trigger_data_receiver,
253 test_values.connection_selection_requester,
254 test_values.roam_request_sender,
255 test_values.telemetry_sender,
256 test_values.past_roams,
257 );
258 pin_mut!(serve_fut);
259
260 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
262
263 test_values
266 .trigger_data_sender
267 .try_send(RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
268 rssi_dbm: Some(-40),
269 snr_db: Some(40),
270 tx_rate_500kbps: Some(0),
271 ..Default::default()
272 }))
273 .expect("failed to send");
274
275 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
277
278 match response_to_should_roam_scan {
279 RoamTriggerDataOutcome::RoamSearch { .. } => {
280 assert_matches!(
282 test_values.telemetry_receiver.try_recv(),
283 Ok(TelemetryEvent::PolicyRoamScan { .. })
284 );
285 assert_matches!(
287 test_values.connection_selection_request_receiver.try_recv(),
288 Ok(_)
289 );
290 }
291 RoamTriggerDataOutcome::Noop => {
292 assert_matches!(
294 test_values.connection_selection_request_receiver.try_recv(),
295 Err(_)
296 );
297 }
298 }
299 }
300
301 #[test_case(false, RoamingMode::CanRoam; "should not send roam request can roam")]
302 #[test_case(false, RoamingMode::MetricsOnly; "should not send roam request metrics only")]
303 #[test_case(true, RoamingMode::CanRoam; "should send roam request can roam")]
304 #[test_case(true, RoamingMode::MetricsOnly; "should send roam request metrics only")]
305 #[fuchsia::test(add_test_attr = false)]
306 fn test_serve_loop_handles_roam_search_results(
307 response_to_should_send_roam_request: bool,
308 roaming_mode: RoamingMode,
309 ) {
310 let mut exec = TestExecutor::new();
311 let mut test_values = setup_test();
312
313 let mut roam_monitor = FakeRoamMonitor::new();
316 roam_monitor.response_to_should_roam_scan = RoamTriggerDataOutcome::RoamSearch {
317 scan_type: fidl_common::ScanType::Passive,
318 network_identifier: generate_random_network_identifier(),
319 credential: generate_random_password(),
320 current_security: types::SecurityTypeDetailed::Open,
321 reasons: vec![],
322 };
323 roam_monitor.response_to_should_send_roam_request = response_to_should_send_roam_request;
324
325 let serve_fut = serve_roam_monitor(
327 Box::new(roam_monitor),
328 RoamingPolicy::Enabled { profile: RoamingProfile::Stationary, mode: roaming_mode },
329 test_values.trigger_data_receiver,
330 test_values.connection_selection_requester,
331 test_values.roam_request_sender,
332 test_values.telemetry_sender,
333 test_values.past_roams,
334 );
335 pin_mut!(serve_fut);
336
337 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
339
340 test_values
343 .trigger_data_sender
344 .try_send(RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
345 rssi_dbm: Some(-40),
346 snr_db: Some(40),
347 tx_rate_500kbps: Some(0),
348 ..Default::default()
349 }))
350 .expect("failed to send");
351
352 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
354
355 let candidate = generate_random_scanned_candidate();
357 assert_matches!(test_values.connection_selection_request_receiver.try_recv(), Ok(ConnectionSelectionRequest::RoamSelection { responder, .. }) => {
358 responder.send(Some(candidate.clone())).expect("failed to send");
360 });
361
362 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
364
365 assert_matches!(
367 test_values.telemetry_receiver.try_recv(),
368 Ok(TelemetryEvent::PolicyRoamScan { .. })
369 );
370
371 if response_to_should_send_roam_request && roaming_mode == RoamingMode::CanRoam {
372 assert_matches!(test_values.roam_request_receiver.try_recv(), Ok(selection) => {
375 assert_eq!(selection.candidate, candidate);
376 });
377 } else {
378 assert_matches!(test_values.roam_request_receiver.try_recv(), Err(_));
381 }
382 }
383
384 #[fuchsia::test]
385 fn test_roam_attempts_are_recorded_in_past_roams() {
386 let mut exec = TestExecutor::new();
387 let mut test_values = setup_test();
388
389 let mut roam_monitor = FakeRoamMonitor::new();
392 roam_monitor.response_to_should_roam_scan = RoamTriggerDataOutcome::RoamSearch {
393 scan_type: fidl_common::ScanType::Passive,
394 network_identifier: generate_random_network_identifier(),
395 credential: generate_random_password(),
396 current_security: types::SecurityTypeDetailed::Open,
397 reasons: vec![],
398 };
399 roam_monitor.response_to_should_send_roam_request = true;
400
401 let serve_fut = serve_roam_monitor(
403 Box::new(roam_monitor),
404 RoamingPolicy::Enabled {
405 profile: RoamingProfile::Stationary,
406 mode: RoamingMode::CanRoam,
407 },
408 test_values.trigger_data_receiver,
409 test_values.connection_selection_requester,
410 test_values.roam_request_sender,
411 test_values.telemetry_sender,
412 test_values.past_roams.clone(),
413 );
414 pin_mut!(serve_fut);
415
416 trigger_scan_and_roam(
417 &mut exec,
418 &mut serve_fut,
419 &mut test_values.trigger_data_sender,
420 &mut test_values.connection_selection_request_receiver,
421 &mut test_values.roam_request_receiver,
422 );
423
424 let past_roams = test_values
426 .past_roams
427 .clone()
428 .try_lock()
429 .unwrap()
430 .get_recent(fasync::MonotonicInstant::INFINITE_PAST);
431 assert_eq!(past_roams.len(), 1);
432
433 trigger_scan_and_roam(
434 &mut exec,
435 &mut serve_fut,
436 &mut test_values.trigger_data_sender,
437 &mut test_values.connection_selection_request_receiver,
438 &mut test_values.roam_request_receiver,
439 );
440
441 let past_roams = test_values
443 .past_roams
444 .clone()
445 .try_lock()
446 .unwrap()
447 .get_recent(fasync::MonotonicInstant::INFINITE_PAST);
448 assert_eq!(past_roams.len(), 2);
449 }
450
451 fn trigger_scan_and_roam(
455 exec: &mut TestExecutor,
456 serve_fut: &mut Pin<&mut impl Future<Output = std::result::Result<(), anyhow::Error>>>,
457 trigger_data_sender: &mut mpsc::Sender<RoamTriggerData>,
458 connection_selection_request_receiver: &mut mpsc::Receiver<ConnectionSelectionRequest>,
459 roam_request_receiver: &mut mpsc::Receiver<PolicyRoamRequest>,
460 ) {
461 assert_matches!(exec.run_until_stalled(serve_fut), Poll::Pending);
463
464 trigger_data_sender
467 .try_send(RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
468 rssi_dbm: Some(-40),
469 snr_db: Some(40),
470 tx_rate_500kbps: Some(0),
471 ..Default::default()
472 }))
473 .expect("failed to send");
474
475 assert_matches!(exec.run_until_stalled(serve_fut), Poll::Pending);
477
478 let candidate = generate_random_scanned_candidate();
480 assert_matches!(connection_selection_request_receiver.try_recv(), Ok(ConnectionSelectionRequest::RoamSelection { responder, .. }) => {
481 responder.send(Some(candidate.clone())).expect("failed to send");
483 });
484
485 assert_matches!(exec.run_until_stalled(serve_fut), Poll::Pending);
486 assert_matches!(roam_request_receiver.try_recv(), Ok(selection) => {
487 assert_eq!(selection.candidate, candidate);
488 });
489 }
490}