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 = 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        // Verify that roam sender packages trigger data and sends to roam monitor receiver.
228        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        // Create a fake roam monitor. Set the should_roam_scan response, so we can verify that the
241        // serve loop forwarded the data and that the correct action was taken.
242        let mut roam_monitor = FakeRoamMonitor::new();
243        roam_monitor.response_to_should_roam_scan = response_to_should_roam_scan.clone();
244
245        // Start a serve loop with the fake roam monitor
246        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        // Run loop forward
261        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
262
263        // Send some trigger data to kick off the handling sequence. The actual values here are
264        // irrelevant.
265        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        // Run loop forward.
276        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
277
278        match response_to_should_roam_scan {
279            RoamTriggerDataOutcome::RoamSearch { .. } => {
280                // Verify metric was sent for upcoming roam scan
281                assert_matches!(
282                    test_values.telemetry_receiver.try_recv(),
283                    Ok(TelemetryEvent::PolicyRoamScan { .. })
284                );
285                // Verify that a roam search request was sent after monitor responded true.
286                assert_matches!(
287                    test_values.connection_selection_request_receiver.try_recv(),
288                    Ok(_)
289                );
290            }
291            RoamTriggerDataOutcome::Noop => {
292                // Verify that no roam search was triggered after monitor responded false.
293                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        // Create a fake roam monitor. Set should_roam_scan to true to ensure roam searches get
314        // queued. Conditionally set the should_send_roam_request response.
315        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        // Start a serve loop with the fake roam monitor
326        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        // Run loop forward
338        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
339
340        // Send some trigger data to kick off the handling sequence. The actual values here are
341        // irrelevant.
342        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        // Run loop forward
353        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
354
355        // Respond via the connection selection requester
356        let candidate = generate_random_scanned_candidate();
357        assert_matches!(test_values.connection_selection_request_receiver.try_recv(), Ok(ConnectionSelectionRequest::RoamSelection { responder, .. }) => {
358            // Respond with a roam candidate
359            responder.send(Some(candidate.clone())).expect("failed to send");
360        });
361
362        // Run loop forward
363        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
364
365        // Verify metric was sent for upcoming roam scan
366        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            // Verify that a roam request is sent if the should_send_roam_request method returns
373            // true.
374            assert_matches!(test_values.roam_request_receiver.try_recv(), Ok(selection) => {
375                assert_eq!(selection.candidate, candidate);
376            });
377        } else {
378            // Verify that no roam request is sent if the should_send_roam_request method returns
379            // false, regardless of the roaming mode.
380            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        // Create a fake roam monitor. Set should_roam_scan to true to ensure roam searches get
390        // queued. Conditionally set the should_send_roam_request response.
391        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        // Start a serve loop with the fake roam monitor
402        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        // A roam request should have been sent, verify that it is recorded in the past roams list.
425        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        // A roam request should have been sent, verify that it is recorded in the past roams list.
442        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    // This sends a signal report to the roam monitor, responds to roam searches with a random
452    // candidate network, progresses the serve loop forward, and verifies that the roam monitor
453    // sends out a roam request.
454    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        // Run the serve loop forward
462        assert_matches!(exec.run_until_stalled(serve_fut), Poll::Pending);
463
464        // Send some trigger data to kick off the handling sequence. The actual values here are
465        // irrelevant.
466        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        // Run loop forward
476        assert_matches!(exec.run_until_stalled(serve_fut), Poll::Pending);
477
478        // Respond via the connection selection requester
479        let candidate = generate_random_scanned_candidate();
480        assert_matches!(connection_selection_request_receiver.try_recv(), Ok(ConnectionSelectionRequest::RoamSelection { responder, .. }) => {
481            // Respond with a roam candidate
482            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}