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 futures::channel::mpsc;
13use futures::future::LocalBoxFuture;
14use futures::lock::Mutex;
15use futures::stream::{FuturesUnordered, StreamExt};
16use futures::{FutureExt, select};
17use log::{debug, error, info, warn};
18use std::any::Any;
19use std::sync::Arc;
20use {fidl_fuchsia_wlan_common as fidl_common, fidl_fuchsia_wlan_internal as fidl_internal};
21
22pub mod default_monitor;
23pub mod stationary_monitor;
24
25// Struct to expose methods for state machine to send roam data, regardless of roam profile.
26pub struct RoamDataSender {
27    sender: mpsc::Sender<RoamTriggerData>,
28}
29impl RoamDataSender {
30    pub fn new(trigger_data_sender: mpsc::Sender<RoamTriggerData>) -> Self {
31        Self { sender: trigger_data_sender }
32    }
33    pub fn send_signal_report_ind(
34        &mut self,
35        ind: fidl_internal::SignalReportIndication,
36    ) -> Result<(), anyhow::Error> {
37        Ok(self.sender.try_send(RoamTriggerData::SignalReportInd(ind))?)
38    }
39}
40/// Trait for creating different roam monitors based on roaming profiles.
41#[async_trait(?Send)]
42pub trait RoamMonitorApi: Any {
43    // Handles trigger data and evaluates current state. Returns an outcome to be taken (e.g. if
44    // roam search is warranted). All roam monitors MUST handle all trigger data types, even if
45    // they always take no action.
46    async fn handle_roam_trigger_data(
47        &mut self,
48        data: RoamTriggerData,
49    ) -> Result<RoamTriggerDataOutcome, anyhow::Error>;
50    // Determines if the selected roam candidate is still relevant and provides enough potential
51    // improvement to warrant a roam. Returns true if roam request should be sent to state machine.
52    fn should_send_roam_request(&self, request: PolicyRoamRequest) -> Result<bool, anyhow::Error>;
53    // Method to inform roam monitor of a roam being attempted, so it may  reset any internal state
54    // as necessary.
55    fn notify_of_roam_attempt(&mut self);
56}
57
58// Service loop that orchestrates interaction between state machine (incoming roam data and outgoing
59// roam requests), roam monitor implementation, and roam manager (roam search requests and results).
60pub async fn serve_roam_monitor(
61    mut roam_monitor: Box<dyn RoamMonitorApi>,
62    roaming_policy: RoamingPolicy,
63    mut trigger_data_receiver: mpsc::Receiver<RoamTriggerData>,
64    connection_selection_requester: ConnectionSelectionRequester,
65    mut roam_request_sender: mpsc::Sender<PolicyRoamRequest>,
66    telemetry_sender: TelemetrySender,
67    past_roams: Arc<Mutex<PastRoamList>>,
68) -> Result<(), anyhow::Error> {
69    // Queue of initialized roam searches.
70    let mut roam_search_result_futs: FuturesUnordered<
71        LocalBoxFuture<'static, Result<PolicyRoamRequest, Error>>,
72    > = FuturesUnordered::new();
73
74    loop {
75        select! {
76            // Handle incoming trigger data.
77            trigger_data = trigger_data_receiver.next() => if let Some(data) = trigger_data {
78                match roam_monitor.handle_roam_trigger_data(data).await {
79                    Ok(RoamTriggerDataOutcome::RoamSearch { scan_type, network_identifier, credential, current_security, reasons}) => {
80                        telemetry_sender.send(TelemetryEvent::PolicyRoamScan { reasons: reasons.clone() });
81                        info!("Performing scan to find proactive local roaming candidates.");
82                        let roam_search_fut = get_roaming_connection_selection_future(
83                            connection_selection_requester.clone(),
84                            scan_type,
85                            network_identifier,
86                            credential,
87                            current_security,
88                            reasons
89                        );
90                        roam_search_result_futs.push(roam_search_fut.boxed());
91                    },
92                    Ok(RoamTriggerDataOutcome::Noop) => {},
93                    Err(e) => error!("error handling roam trigger data: {}", e),
94                }
95            },
96            // Handle the result of a completed roam search, sending recommentation to roam if
97            // necessary.
98            roam_search_result = roam_search_result_futs.select_next_some() => match roam_search_result {
99                Ok(request) => {
100                    if roam_monitor.should_send_roam_request(request.clone()).unwrap_or_else(|e| {
101                            error!("Error validating selected roam candidate: {}", e);
102                            false
103                        }) {
104                            match roaming_policy {
105                                RoamingPolicy::Enabled { mode: RoamingMode::CanRoam, ..} => {
106                                    info!("Requesting roam to candidate: {:?}", request.candidate.to_string_without_pii());
107                                    if roam_request_sender.try_send(request).is_err() {
108                                        warn!("Failed to send roam request, exiting monitor service loop.");
109                                        break
110                                    }
111                                }
112                                _ => {
113                                    debug!("Roaming policy is {:?}. Skipping roam request.", roaming_policy);
114                                    telemetry_sender.send(TelemetryEvent::WouldRoamConnect)
115                                }
116                            }
117                            // Record that a roam attempt is made.
118                            if let Some(mut past_roams) = past_roams.try_lock() {
119                                past_roams.add(RoamEvent::new_roam_now());
120                            } else {
121                                error!("Unexpectedly failed to acquire lock on past roam list; will not record roam");
122                            }
123                    }
124                }
125                Err(e) => {
126                    error!("Error occured during roam search: {:?}", e);
127                }
128            },
129            complete => {
130                debug!("Roam monitor channels dropped, exiting monitor service loop.");
131                break
132            }
133        }
134    }
135    Ok(())
136}
137
138// Request a roam selection from the connection selection module, bundle the receiver into a future
139// to be queued and that can also return the initiating request.
140async fn get_roaming_connection_selection_future(
141    mut connection_selection_requester: ConnectionSelectionRequester,
142    scan_type: fidl_common::ScanType,
143    network_identifier: types::NetworkIdentifier,
144    credential: Credential,
145    current_security: types::SecurityTypeDetailed,
146    reasons: Vec<RoamReason>,
147) -> Result<PolicyRoamRequest, Error> {
148    match connection_selection_requester
149        .do_roam_selection(scan_type, network_identifier, credential, current_security)
150        .await?
151    {
152        Some(candidate) => Ok(PolicyRoamRequest { candidate, reasons }),
153        None => Err(format_err!("No roam candidates found.")),
154    }
155}
156
157#[cfg(test)]
158mod test {
159    use super::*;
160    use crate::client::connection_selection::ConnectionSelectionRequest;
161    use crate::client::roaming::lib::{NUM_PLATFORM_MAX_ROAMS_PER_DAY, RoamingProfile};
162    use crate::telemetry::TelemetryEvent;
163    use crate::util::testing::fakes::FakeRoamMonitor;
164    use crate::util::testing::{
165        generate_random_network_identifier, generate_random_password,
166        generate_random_scanned_candidate,
167    };
168    use assert_matches::assert_matches;
169    use fuchsia_async::{self as fasync, TestExecutor};
170    use futures::task::Poll;
171    use futures::{Future, pin_mut};
172    use std::pin::Pin;
173    use test_case::test_case;
174    use {fidl_fuchsia_wlan_common as fidl_common, fidl_fuchsia_wlan_internal as fidl_internal};
175
176    struct TestValues {
177        trigger_data_sender: mpsc::Sender<RoamTriggerData>,
178        trigger_data_receiver: mpsc::Receiver<RoamTriggerData>,
179        roam_request_sender: mpsc::Sender<PolicyRoamRequest>,
180        roam_request_receiver: mpsc::Receiver<PolicyRoamRequest>,
181        connection_selection_requester: ConnectionSelectionRequester,
182        connection_selection_request_receiver: mpsc::Receiver<ConnectionSelectionRequest>,
183        telemetry_sender: TelemetrySender,
184        telemetry_receiver: mpsc::Receiver<TelemetryEvent>,
185        past_roams: Arc<Mutex<PastRoamList>>,
186    }
187
188    fn setup_test() -> TestValues {
189        let (trigger_data_sender, trigger_data_receiver) = mpsc::channel(100);
190        let (roam_sender, roam_receiver) = mpsc::channel(100);
191        let (connection_selection_request_sender, connection_selection_request_receiver) =
192            mpsc::channel(5);
193        let connection_selection_requester =
194            ConnectionSelectionRequester::new(connection_selection_request_sender);
195        let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
196        let telemetry_sender = TelemetrySender::new(telemetry_sender);
197        let past_roams = Arc::new(Mutex::new(PastRoamList::new(NUM_PLATFORM_MAX_ROAMS_PER_DAY)));
198        TestValues {
199            trigger_data_sender,
200            trigger_data_receiver,
201            roam_request_sender: roam_sender,
202            roam_request_receiver: roam_receiver,
203            connection_selection_requester,
204            connection_selection_request_receiver,
205            telemetry_sender,
206            telemetry_receiver,
207            past_roams,
208        }
209    }
210
211    #[fuchsia::test]
212    fn test_roam_data_sender_send_signal_report_ind() {
213        let _exec = TestExecutor::new();
214        let (sender, mut receiver) = mpsc::channel(100);
215        let mut roam_data_sender = RoamDataSender::new(sender);
216        let ind = fidl_internal::SignalReportIndication { rssi_dbm: -60, snr_db: 30 };
217
218        roam_data_sender.send_signal_report_ind(ind).expect("error sending signal report");
219
220        // Verify that roam sender packages trigger data and sends to roam monitor receiver.
221        assert_matches!(receiver.try_next(), Ok(Some(RoamTriggerData::SignalReportInd(data))) => {
222            assert_eq!(ind, data);
223        });
224    }
225
226    #[test_case(RoamTriggerDataOutcome::Noop; "should not queue roam search")]
227    #[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")]
228    #[fuchsia::test(add_test_attr = false)]
229    fn test_serve_loop_handles_trigger_data(response_to_should_roam_scan: RoamTriggerDataOutcome) {
230        let mut exec = TestExecutor::new();
231        let mut test_values = setup_test();
232
233        // Create a fake roam monitor. Set the should_roam_scan response, so we can verify that the
234        // serve loop forwarded the data and that the correct action was taken.
235        let mut roam_monitor = FakeRoamMonitor::new();
236        roam_monitor.response_to_should_roam_scan = response_to_should_roam_scan.clone();
237
238        // Start a serve loop with the fake roam monitor
239        let serve_fut = serve_roam_monitor(
240            Box::new(roam_monitor),
241            RoamingPolicy::Enabled {
242                profile: RoamingProfile::Stationary,
243                mode: RoamingMode::CanRoam,
244            },
245            test_values.trigger_data_receiver,
246            test_values.connection_selection_requester,
247            test_values.roam_request_sender,
248            test_values.telemetry_sender,
249            test_values.past_roams,
250        );
251        pin_mut!(serve_fut);
252
253        // Run loop forward
254        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
255
256        // Send some trigger data to kick off the handling sequence. The actual values here are
257        // irrelevant.
258        test_values
259            .trigger_data_sender
260            .try_send(RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
261                rssi_dbm: -40,
262                snr_db: 40,
263            }))
264            .expect("failed to send");
265
266        // Run loop forward.
267        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
268
269        match response_to_should_roam_scan {
270            RoamTriggerDataOutcome::RoamSearch { .. } => {
271                // Verify metric was sent for upcoming roam scan
272                assert_matches!(
273                    test_values.telemetry_receiver.try_next(),
274                    Ok(Some(TelemetryEvent::PolicyRoamScan { .. }))
275                );
276                // Verify that a roam search request was sent after monitor responded true.
277                assert_matches!(
278                    test_values.connection_selection_request_receiver.try_next(),
279                    Ok(Some(_))
280                );
281            }
282            RoamTriggerDataOutcome::Noop => {
283                // Verify that no roam search was triggered after monitor responded false.
284                assert_matches!(
285                    test_values.connection_selection_request_receiver.try_next(),
286                    Err(_)
287                );
288            }
289        }
290    }
291
292    #[test_case(false, RoamingMode::CanRoam; "should not send roam request can roam")]
293    #[test_case(false, RoamingMode::MetricsOnly; "should not send roam request metrics only")]
294    #[test_case(true, RoamingMode::CanRoam; "should send roam request can roam")]
295    #[test_case(true, RoamingMode::MetricsOnly; "should send roam request metrics only")]
296    #[fuchsia::test(add_test_attr = false)]
297    fn test_serve_loop_handles_roam_search_results(
298        response_to_should_send_roam_request: bool,
299        roaming_mode: RoamingMode,
300    ) {
301        let mut exec = TestExecutor::new();
302        let mut test_values = setup_test();
303
304        // Create a fake roam monitor. Set should_roam_scan to true to ensure roam searches get
305        // queued. Conditionally set the should_send_roam_request response.
306        let mut roam_monitor = FakeRoamMonitor::new();
307        roam_monitor.response_to_should_roam_scan = RoamTriggerDataOutcome::RoamSearch {
308            scan_type: fidl_common::ScanType::Passive,
309            network_identifier: generate_random_network_identifier(),
310            credential: generate_random_password(),
311            current_security: types::SecurityTypeDetailed::Open,
312            reasons: vec![],
313        };
314        roam_monitor.response_to_should_send_roam_request = response_to_should_send_roam_request;
315
316        // Start a serve loop with the fake roam monitor
317        let serve_fut = serve_roam_monitor(
318            Box::new(roam_monitor),
319            RoamingPolicy::Enabled { profile: RoamingProfile::Stationary, mode: roaming_mode },
320            test_values.trigger_data_receiver,
321            test_values.connection_selection_requester,
322            test_values.roam_request_sender,
323            test_values.telemetry_sender,
324            test_values.past_roams,
325        );
326        pin_mut!(serve_fut);
327
328        // Run loop forward
329        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
330
331        // Send some trigger data to kick off the handling sequence. The actual values here are
332        // irrelevant.
333        test_values
334            .trigger_data_sender
335            .try_send(RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
336                rssi_dbm: -40,
337                snr_db: 40,
338            }))
339            .expect("failed to send");
340
341        // Run loop forward
342        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
343
344        // Respond via the connection selection requester
345        let candidate = generate_random_scanned_candidate();
346        assert_matches!(test_values.connection_selection_request_receiver.try_next(), Ok(Some(ConnectionSelectionRequest::RoamSelection { responder, .. })) => {
347            // Respond with a roam candidate
348            responder.send(Some(candidate.clone())).expect("failed to send");
349        });
350
351        // Run loop forward
352        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
353
354        // Verify metric was sent for upcoming roam scan
355        assert_matches!(
356            test_values.telemetry_receiver.try_next(),
357            Ok(Some(TelemetryEvent::PolicyRoamScan { .. }))
358        );
359
360        if response_to_should_send_roam_request && roaming_mode == RoamingMode::CanRoam {
361            // Verify that a roam request is sent if the should_send_roam_request method returns
362            // true.
363            assert_matches!(test_values.roam_request_receiver.try_next(), Ok(Some(selection)) => {
364                assert_eq!(selection.candidate, candidate);
365            });
366        } else {
367            // Verify that no roam request is sent if the should_send_roam_request method returns
368            // false, regardless of the roaming mode.
369            assert_matches!(test_values.roam_request_receiver.try_next(), Err(_));
370        }
371    }
372
373    #[fuchsia::test]
374    fn test_roam_attempts_are_recorded_in_past_roams() {
375        let mut exec = TestExecutor::new();
376        let mut test_values = setup_test();
377
378        // Create a fake roam monitor. Set should_roam_scan to true to ensure roam searches get
379        // queued. Conditionally set the should_send_roam_request response.
380        let mut roam_monitor = FakeRoamMonitor::new();
381        roam_monitor.response_to_should_roam_scan = RoamTriggerDataOutcome::RoamSearch {
382            scan_type: fidl_common::ScanType::Passive,
383            network_identifier: generate_random_network_identifier(),
384            credential: generate_random_password(),
385            current_security: types::SecurityTypeDetailed::Open,
386            reasons: vec![],
387        };
388        roam_monitor.response_to_should_send_roam_request = true;
389
390        // Start a serve loop with the fake roam monitor
391        let serve_fut = serve_roam_monitor(
392            Box::new(roam_monitor),
393            RoamingPolicy::Enabled {
394                profile: RoamingProfile::Stationary,
395                mode: RoamingMode::CanRoam,
396            },
397            test_values.trigger_data_receiver,
398            test_values.connection_selection_requester,
399            test_values.roam_request_sender,
400            test_values.telemetry_sender,
401            test_values.past_roams.clone(),
402        );
403        pin_mut!(serve_fut);
404
405        trigger_scan_and_roam(
406            &mut exec,
407            &mut serve_fut,
408            &mut test_values.trigger_data_sender,
409            &mut test_values.connection_selection_request_receiver,
410            &mut test_values.roam_request_receiver,
411        );
412
413        // A roam request should have been sent, verify that it is recorded in the past roams list.
414        let past_roams = test_values
415            .past_roams
416            .clone()
417            .try_lock()
418            .unwrap()
419            .get_recent(fasync::MonotonicInstant::INFINITE_PAST);
420        assert_eq!(past_roams.len(), 1);
421
422        trigger_scan_and_roam(
423            &mut exec,
424            &mut serve_fut,
425            &mut test_values.trigger_data_sender,
426            &mut test_values.connection_selection_request_receiver,
427            &mut test_values.roam_request_receiver,
428        );
429
430        // A roam request should have been sent, verify that it is recorded in the past roams list.
431        let past_roams = test_values
432            .past_roams
433            .clone()
434            .try_lock()
435            .unwrap()
436            .get_recent(fasync::MonotonicInstant::INFINITE_PAST);
437        assert_eq!(past_roams.len(), 2);
438    }
439
440    // This sends a signal report to the roam monitor, responds to roam searches with a random
441    // candidate network, progresses the serve loop forward, and verifies that the roam monitor
442    // sends out a roam request.
443    fn trigger_scan_and_roam(
444        exec: &mut TestExecutor,
445        serve_fut: &mut Pin<&mut impl Future<Output = std::result::Result<(), anyhow::Error>>>,
446        trigger_data_sender: &mut mpsc::Sender<RoamTriggerData>,
447        connection_selection_request_receiver: &mut mpsc::Receiver<ConnectionSelectionRequest>,
448        roam_request_receiver: &mut mpsc::Receiver<PolicyRoamRequest>,
449    ) {
450        // Run the serve loop forward
451        assert_matches!(exec.run_until_stalled(serve_fut), Poll::Pending);
452
453        // Send some trigger data to kick off the handling sequence. The actual values here are
454        // irrelevant.
455        trigger_data_sender
456            .try_send(RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
457                rssi_dbm: -40,
458                snr_db: 40,
459            }))
460            .expect("failed to send");
461
462        // Run loop forward
463        assert_matches!(exec.run_until_stalled(serve_fut), Poll::Pending);
464
465        // Respond via the connection selection requester
466        let candidate = generate_random_scanned_candidate();
467        assert_matches!(connection_selection_request_receiver.try_next(), Ok(Some(ConnectionSelectionRequest::RoamSelection { responder, .. })) => {
468            // Respond with a roam candidate
469            responder.send(Some(candidate.clone())).expect("failed to send");
470        });
471
472        assert_matches!(exec.run_until_stalled(serve_fut), Poll::Pending);
473        assert_matches!(roam_request_receiver.try_next(), Ok(Some(selection)) => {
474            assert_eq!(selection.candidate, candidate);
475        });
476    }
477}