Skip to main content

wlancfg_lib/client/roaming/roam_monitor/
mod.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use 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
26// Struct to expose methods for state machine to send roam data, regardless of roam profile.
27pub 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/// Trait for creating different roam monitors based on roaming profiles.
42#[async_trait(?Send)]
43pub trait RoamMonitorApi: Any {
44    // Handles trigger data and evaluates current state. Returns an outcome to be taken (e.g. if
45    // roam search is warranted). All roam monitors MUST handle all trigger data types, even if
46    // they always take no action.
47    async fn handle_roam_trigger_data(
48        &mut self,
49        data: RoamTriggerData,
50    ) -> Result<RoamTriggerDataOutcome, anyhow::Error>;
51    // Determines if the selected roam candidate is still relevant and provides enough potential
52    // improvement to warrant a roam. Returns true if roam request should be sent to state machine.
53    fn should_send_roam_request(&self, request: PolicyRoamRequest) -> Result<bool, anyhow::Error>;
54    // Method to inform roam monitor of a roam being attempted, so it may  reset any internal state
55    // as necessary.
56    fn notify_of_roam_attempt(&mut self);
57}
58
59// Service loop that orchestrates interaction between state machine (incoming roam data and outgoing
60// roam requests), roam monitor implementation, and roam manager (roam search requests and results).
61pub 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    // Queue of initialized roam searches.
71    let mut roam_search_result_futs: FuturesUnordered<
72        LocalBoxFuture<'static, Result<PolicyRoamRequest, Error>>,
73    > = FuturesUnordered::new();
74
75    loop {
76        select! {
77            // Handle incoming trigger data.
78            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            // Handle the result of a completed roam search, sending recommentation to roam if
98            // necessary.
99            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                            // Record that a roam attempt is made.
119                            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
139// Request a roam selection from the connection selection module, bundle the receiver into a future
140// to be queued and that can also return the initiating request.
141async 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 =
219            fidl_internal::SignalReportIndication { rssi_dbm: -60, snr_db: 30, tx_rate_500kbps: 0 };
220
221        roam_data_sender.send_signal_report_ind(ind).expect("error sending signal report");
222
223        // Verify that roam sender packages trigger data and sends to roam monitor receiver.
224        assert_matches!(receiver.try_recv(), Ok(RoamTriggerData::SignalReportInd(data)) => {
225            assert_eq!(ind, data);
226        });
227    }
228
229    #[test_case(RoamTriggerDataOutcome::Noop; "should not queue roam search")]
230    #[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")]
231    #[fuchsia::test(add_test_attr = false)]
232    fn test_serve_loop_handles_trigger_data(response_to_should_roam_scan: RoamTriggerDataOutcome) {
233        let mut exec = TestExecutor::new();
234        let mut test_values = setup_test();
235
236        // Create a fake roam monitor. Set the should_roam_scan response, so we can verify that the
237        // serve loop forwarded the data and that the correct action was taken.
238        let mut roam_monitor = FakeRoamMonitor::new();
239        roam_monitor.response_to_should_roam_scan = response_to_should_roam_scan.clone();
240
241        // Start a serve loop with the fake roam monitor
242        let serve_fut = serve_roam_monitor(
243            Box::new(roam_monitor),
244            RoamingPolicy::Enabled {
245                profile: RoamingProfile::Stationary,
246                mode: RoamingMode::CanRoam,
247            },
248            test_values.trigger_data_receiver,
249            test_values.connection_selection_requester,
250            test_values.roam_request_sender,
251            test_values.telemetry_sender,
252            test_values.past_roams,
253        );
254        pin_mut!(serve_fut);
255
256        // Run loop forward
257        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
258
259        // Send some trigger data to kick off the handling sequence. The actual values here are
260        // irrelevant.
261        test_values
262            .trigger_data_sender
263            .try_send(RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
264                rssi_dbm: -40,
265                snr_db: 40,
266                tx_rate_500kbps: 0,
267            }))
268            .expect("failed to send");
269
270        // Run loop forward.
271        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
272
273        match response_to_should_roam_scan {
274            RoamTriggerDataOutcome::RoamSearch { .. } => {
275                // Verify metric was sent for upcoming roam scan
276                assert_matches!(
277                    test_values.telemetry_receiver.try_recv(),
278                    Ok(TelemetryEvent::PolicyRoamScan { .. })
279                );
280                // Verify that a roam search request was sent after monitor responded true.
281                assert_matches!(
282                    test_values.connection_selection_request_receiver.try_recv(),
283                    Ok(_)
284                );
285            }
286            RoamTriggerDataOutcome::Noop => {
287                // Verify that no roam search was triggered after monitor responded false.
288                assert_matches!(
289                    test_values.connection_selection_request_receiver.try_recv(),
290                    Err(_)
291                );
292            }
293        }
294    }
295
296    #[test_case(false, RoamingMode::CanRoam; "should not send roam request can roam")]
297    #[test_case(false, RoamingMode::MetricsOnly; "should not send roam request metrics only")]
298    #[test_case(true, RoamingMode::CanRoam; "should send roam request can roam")]
299    #[test_case(true, RoamingMode::MetricsOnly; "should send roam request metrics only")]
300    #[fuchsia::test(add_test_attr = false)]
301    fn test_serve_loop_handles_roam_search_results(
302        response_to_should_send_roam_request: bool,
303        roaming_mode: RoamingMode,
304    ) {
305        let mut exec = TestExecutor::new();
306        let mut test_values = setup_test();
307
308        // Create a fake roam monitor. Set should_roam_scan to true to ensure roam searches get
309        // queued. Conditionally set the should_send_roam_request response.
310        let mut roam_monitor = FakeRoamMonitor::new();
311        roam_monitor.response_to_should_roam_scan = RoamTriggerDataOutcome::RoamSearch {
312            scan_type: fidl_common::ScanType::Passive,
313            network_identifier: generate_random_network_identifier(),
314            credential: generate_random_password(),
315            current_security: types::SecurityTypeDetailed::Open,
316            reasons: vec![],
317        };
318        roam_monitor.response_to_should_send_roam_request = response_to_should_send_roam_request;
319
320        // Start a serve loop with the fake roam monitor
321        let serve_fut = serve_roam_monitor(
322            Box::new(roam_monitor),
323            RoamingPolicy::Enabled { profile: RoamingProfile::Stationary, mode: roaming_mode },
324            test_values.trigger_data_receiver,
325            test_values.connection_selection_requester,
326            test_values.roam_request_sender,
327            test_values.telemetry_sender,
328            test_values.past_roams,
329        );
330        pin_mut!(serve_fut);
331
332        // Run loop forward
333        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
334
335        // Send some trigger data to kick off the handling sequence. The actual values here are
336        // irrelevant.
337        test_values
338            .trigger_data_sender
339            .try_send(RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
340                rssi_dbm: -40,
341                snr_db: 40,
342                tx_rate_500kbps: 0,
343            }))
344            .expect("failed to send");
345
346        // Run loop forward
347        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
348
349        // Respond via the connection selection requester
350        let candidate = generate_random_scanned_candidate();
351        assert_matches!(test_values.connection_selection_request_receiver.try_recv(), Ok(ConnectionSelectionRequest::RoamSelection { responder, .. }) => {
352            // Respond with a roam candidate
353            responder.send(Some(candidate.clone())).expect("failed to send");
354        });
355
356        // Run loop forward
357        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
358
359        // Verify metric was sent for upcoming roam scan
360        assert_matches!(
361            test_values.telemetry_receiver.try_recv(),
362            Ok(TelemetryEvent::PolicyRoamScan { .. })
363        );
364
365        if response_to_should_send_roam_request && roaming_mode == RoamingMode::CanRoam {
366            // Verify that a roam request is sent if the should_send_roam_request method returns
367            // true.
368            assert_matches!(test_values.roam_request_receiver.try_recv(), Ok(selection) => {
369                assert_eq!(selection.candidate, candidate);
370            });
371        } else {
372            // Verify that no roam request is sent if the should_send_roam_request method returns
373            // false, regardless of the roaming mode.
374            assert_matches!(test_values.roam_request_receiver.try_recv(), Err(_));
375        }
376    }
377
378    #[fuchsia::test]
379    fn test_roam_attempts_are_recorded_in_past_roams() {
380        let mut exec = TestExecutor::new();
381        let mut test_values = setup_test();
382
383        // Create a fake roam monitor. Set should_roam_scan to true to ensure roam searches get
384        // queued. Conditionally set the should_send_roam_request response.
385        let mut roam_monitor = FakeRoamMonitor::new();
386        roam_monitor.response_to_should_roam_scan = RoamTriggerDataOutcome::RoamSearch {
387            scan_type: fidl_common::ScanType::Passive,
388            network_identifier: generate_random_network_identifier(),
389            credential: generate_random_password(),
390            current_security: types::SecurityTypeDetailed::Open,
391            reasons: vec![],
392        };
393        roam_monitor.response_to_should_send_roam_request = true;
394
395        // Start a serve loop with the fake roam monitor
396        let serve_fut = serve_roam_monitor(
397            Box::new(roam_monitor),
398            RoamingPolicy::Enabled {
399                profile: RoamingProfile::Stationary,
400                mode: RoamingMode::CanRoam,
401            },
402            test_values.trigger_data_receiver,
403            test_values.connection_selection_requester,
404            test_values.roam_request_sender,
405            test_values.telemetry_sender,
406            test_values.past_roams.clone(),
407        );
408        pin_mut!(serve_fut);
409
410        trigger_scan_and_roam(
411            &mut exec,
412            &mut serve_fut,
413            &mut test_values.trigger_data_sender,
414            &mut test_values.connection_selection_request_receiver,
415            &mut test_values.roam_request_receiver,
416        );
417
418        // A roam request should have been sent, verify that it is recorded in the past roams list.
419        let past_roams = test_values
420            .past_roams
421            .clone()
422            .try_lock()
423            .unwrap()
424            .get_recent(fasync::MonotonicInstant::INFINITE_PAST);
425        assert_eq!(past_roams.len(), 1);
426
427        trigger_scan_and_roam(
428            &mut exec,
429            &mut serve_fut,
430            &mut test_values.trigger_data_sender,
431            &mut test_values.connection_selection_request_receiver,
432            &mut test_values.roam_request_receiver,
433        );
434
435        // A roam request should have been sent, verify that it is recorded in the past roams list.
436        let past_roams = test_values
437            .past_roams
438            .clone()
439            .try_lock()
440            .unwrap()
441            .get_recent(fasync::MonotonicInstant::INFINITE_PAST);
442        assert_eq!(past_roams.len(), 2);
443    }
444
445    // This sends a signal report to the roam monitor, responds to roam searches with a random
446    // candidate network, progresses the serve loop forward, and verifies that the roam monitor
447    // sends out a roam request.
448    fn trigger_scan_and_roam(
449        exec: &mut TestExecutor,
450        serve_fut: &mut Pin<&mut impl Future<Output = std::result::Result<(), anyhow::Error>>>,
451        trigger_data_sender: &mut mpsc::Sender<RoamTriggerData>,
452        connection_selection_request_receiver: &mut mpsc::Receiver<ConnectionSelectionRequest>,
453        roam_request_receiver: &mut mpsc::Receiver<PolicyRoamRequest>,
454    ) {
455        // Run the serve loop forward
456        assert_matches!(exec.run_until_stalled(serve_fut), Poll::Pending);
457
458        // Send some trigger data to kick off the handling sequence. The actual values here are
459        // irrelevant.
460        trigger_data_sender
461            .try_send(RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
462                rssi_dbm: -40,
463                snr_db: 40,
464                tx_rate_500kbps: 0,
465            }))
466            .expect("failed to send");
467
468        // Run loop forward
469        assert_matches!(exec.run_until_stalled(serve_fut), Poll::Pending);
470
471        // Respond via the connection selection requester
472        let candidate = generate_random_scanned_candidate();
473        assert_matches!(connection_selection_request_receiver.try_recv(), Ok(ConnectionSelectionRequest::RoamSelection { responder, .. }) => {
474            // Respond with a roam candidate
475            responder.send(Some(candidate.clone())).expect("failed to send");
476        });
477
478        assert_matches!(exec.run_until_stalled(serve_fut), Poll::Pending);
479        assert_matches!(roam_request_receiver.try_recv(), Ok(selection) => {
480            assert_eq!(selection.candidate, candidate);
481        });
482    }
483}