blob: ccfda39d2c94121f07529c776462e7f4cffc277b [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use failure::Fail;
use fidl_fuchsia_wlan_common as fidl_common;
use fidl_fuchsia_wlan_mlme::{self as fidl_mlme, BssDescription, ScanRequest, ScanResultCodes};
use log::error;
use std::collections::HashSet;
use std::mem;
use std::sync::Arc;
use crate::client::{DeviceInfo, Ssid};
const PASSIVE_SCAN_MIN_CHANNEL_MS: u32 = 100;
const PASSIVE_SCAN_MAX_CHANNEL_MS: u32 = 200;
const ACTIVE_SCAN_PROBE_DELAY_MS: u32 = 5;
const ACTIVE_SCAN_CHANNEL_MS: u32 = 50;
// Scans can be performed for two different purposes:
// 1) Discover available wireless networks. These scans are initiated by the "user",
// i.e. a client process that talks to SME, and thus have 'tokens' to identify
// a specific user request.
// 2) Join a specific network. These scans can only be initiated by the SME state machine
// itself.
// An SME-initiated scan request for the purpose of joining a network
#[derive(Debug, PartialEq)]
pub struct JoinScan<T> {
pub ssid: Ssid,
pub token: T,
pub scan_type: fidl_common::ScanType,
}
// A "user"-initiated scan request for the purpose of discovering available networks
#[derive(Debug, PartialEq)]
pub struct DiscoveryScan<T> {
tokens: Vec<T>,
scan_type: fidl_common::ScanType,
}
impl<T> DiscoveryScan<T> {
pub fn new(token: T, scan_type: fidl_common::ScanType) -> Self {
Self { tokens: vec![token], scan_type }
}
pub fn matches(&self, scan: &DiscoveryScan<T>) -> bool {
self.scan_type == scan.scan_type
}
pub fn merges(&mut self, mut scan: DiscoveryScan<T>) {
self.tokens.append(&mut scan.tokens)
}
}
pub struct ScanScheduler<D, J> {
// The currently running scan. We assume that MLME can handle a single concurrent scan
// regardless of its own state.
current: ScanState<D, J>,
// A pending scan request for the purpose of joining a network. This type of request
// always takes priority over discovery scans. There can only be one such pending request:
// if SME requests another one, the newer one wins and overwrites an existing request.
pending_join: Option<JoinScan<J>>,
// Pending discovery requests from the user
pending_discovery: Vec<DiscoveryScan<D>>,
device_info: Arc<DeviceInfo>,
last_mlme_txn_id: u64,
}
#[derive(Debug, PartialEq)]
enum ScanState<D, J> {
NotScanning,
ScanningToJoin { cmd: JoinScan<J>, mlme_txn_id: u64, bss_list: Vec<BssDescription> },
ScanningToDiscover { cmd: DiscoveryScan<D>, mlme_txn_id: u64, bss_list: Vec<BssDescription> },
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum JoinScanFailure {
// MLME returned an error code
ScanFailed(ScanResultCodes),
// Scan was canceled or superseded by another request
Canceled,
}
// A reaction to MLME's ScanConfirm event
pub enum ScanResult<D, J> {
// No reaction: the scan results are not relevant anymore, or we received
// an unexpected ScanConfirm.
None,
// "Join" scan has finished, either successfully or not.
// The SME state machine can now send a JoinRequest to MLME if it desires so.
JoinScanFinished { token: J, result: JoinScanResult },
// "Discovery" scan has finished, either successfully or not.
// SME is expected to forward the result to the user.
DiscoveryFinished { tokens: Vec<D>, result: DiscoveryResult },
}
pub type JoinScanResult = Result<Vec<BssDescription>, JoinScanFailure>;
pub type DiscoveryResult = Result<Vec<BssDescription>, DiscoveryError>;
#[derive(Clone, Debug, Fail)]
pub enum DiscoveryError {
#[fail(display = "Scanning not supported by device")]
NotSupported,
#[fail(display = "Internal error occurred")]
InternalError,
}
impl<D, J> ScanScheduler<D, J> {
pub fn new(device_info: Arc<DeviceInfo>) -> Self {
ScanScheduler {
current: ScanState::NotScanning,
pending_join: None,
pending_discovery: Vec::new(),
device_info,
last_mlme_txn_id: 0,
}
}
// Initiate a "join" scan.
//
// If there is an existing pending "join" scan request, it will be discarded and replaced
// with the new one. In this case, the token of the discarded request will be returned.
//
// If there is a currently running "join" scan, its results will be discarded, too.
//
// The scan might or might not begin immediately.
// If a ScanRequest is returned, the caller is responsible for forwarding it to MLME.
pub fn enqueue_scan_to_join(&mut self, s: JoinScan<J>) -> (Option<J>, Option<ScanRequest>) {
let old_token = mem::replace(&mut self.pending_join, Some(s)).map(|p| p.token);
(old_token, self.start_next_scan())
}
// Initiate a "discovery" scan. The scan might or might not begin immediately.
// The request can be merged with any pending or ongoing requests.
// If a ScanRequest is returned, the caller is responsible for forwarding it to MLME.
pub fn enqueue_scan_to_discover(&mut self, s: DiscoveryScan<D>) -> Option<ScanRequest> {
if let ScanState::ScanningToDiscover { cmd, .. } = &mut self.current {
if cmd.matches(&s) {
cmd.merges(s);
return None;
}
}
if let Some(scan_cmd) = self.pending_discovery.iter_mut().find(|cmd| cmd.matches(&s)) {
scan_cmd.merges(s);
return None;
}
self.pending_discovery.push(s);
self.start_next_scan()
}
// Should be called for every OnScanResult event received from MLME.
pub fn on_mlme_scan_result(&mut self, msg: fidl_mlme::ScanResult) {
if !self.matching_mlme_txn_id(msg.txn_id) {
return;
}
match &mut self.current {
ScanState::NotScanning => {}
ScanState::ScanningToJoin { cmd, bss_list, .. } => {
if cmd.ssid == msg.bss.ssid {
bss_list.push(msg.bss)
}
}
ScanState::ScanningToDiscover { bss_list, .. } => bss_list.push(msg.bss),
}
}
// Should be called for every OnScanEnd event received from MLME.
// The caller is expected to take action based on the returned ScanResult.
// If a ScanRequest is returned, the caller is responsible for forwarding it to MLME.
pub fn on_mlme_scan_end(
&mut self,
msg: fidl_mlme::ScanEnd,
) -> (ScanResult<D, J>, Option<ScanRequest>) {
if !self.matching_mlme_txn_id(msg.txn_id) {
return (ScanResult::None, None);
}
let old_state = mem::replace(&mut self.current, ScanState::NotScanning);
let result = match old_state {
ScanState::NotScanning => ScanResult::None,
ScanState::ScanningToJoin { cmd, bss_list, .. } => {
let result = if self.pending_join.is_some() {
// The scan that just finished was superseded by a newer join scan request
Err(JoinScanFailure::Canceled)
} else {
match msg.code {
ScanResultCodes::Success => Ok(bss_list),
other => Err(JoinScanFailure::ScanFailed(other)),
}
};
ScanResult::JoinScanFinished { token: cmd.token, result }
}
ScanState::ScanningToDiscover { cmd, bss_list, .. } => ScanResult::DiscoveryFinished {
tokens: cmd.tokens,
result: convert_discovery_result(msg, bss_list),
},
};
let request = self.start_next_scan();
(result, request)
}
// Returns the most recent join scan request, if there is one.
pub fn get_join_scan(&self) -> Option<&JoinScan<J>> {
if let Some(s) = &self.pending_join {
Some(s)
} else if let ScanState::ScanningToJoin { cmd, .. } = &self.current {
Some(cmd)
} else {
None
}
}
fn matching_mlme_txn_id(&self, incoming_txn_id: u64) -> bool {
match &self.current {
ScanState::NotScanning => false,
ScanState::ScanningToJoin { mlme_txn_id, .. }
| ScanState::ScanningToDiscover { mlme_txn_id, .. } => *mlme_txn_id == incoming_txn_id,
}
}
fn start_next_scan(&mut self) -> Option<ScanRequest> {
match &self.current {
ScanState::NotScanning => {
if let Some(join_scan) = self.pending_join.take() {
self.last_mlme_txn_id += 1;
let request =
new_join_scan_request(self.last_mlme_txn_id, &join_scan, &self.device_info);
self.current = ScanState::ScanningToJoin {
cmd: join_scan,
mlme_txn_id: self.last_mlme_txn_id,
bss_list: Vec::new(),
};
Some(request)
} else if !self.pending_discovery.is_empty() {
self.last_mlme_txn_id += 1;
let scan_cmd = self.pending_discovery.remove(0);
let request = new_discovery_scan_request(
self.last_mlme_txn_id,
&scan_cmd,
&self.device_info,
);
self.current = ScanState::ScanningToDiscover {
cmd: scan_cmd,
mlme_txn_id: self.last_mlme_txn_id,
bss_list: Vec::new(),
};
Some(request)
} else {
None
}
}
_ => None,
}
}
}
const WILDCARD_BSS_ID: [u8; 6] = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
fn new_scan_request(
mlme_txn_id: u64,
scan_type: fidl_common::ScanType,
ssid: Vec<u8>,
device_info: &DeviceInfo,
) -> ScanRequest {
let scan_req = ScanRequest {
txn_id: mlme_txn_id,
bss_type: fidl_mlme::BssTypes::Infrastructure,
bssid: WILDCARD_BSS_ID.clone(),
ssid,
scan_type: fidl_mlme::ScanTypes::Passive,
probe_delay: 0,
channel_list: Some(get_channels_to_scan(&device_info)),
min_channel_time: PASSIVE_SCAN_MIN_CHANNEL_MS,
max_channel_time: PASSIVE_SCAN_MAX_CHANNEL_MS,
ssid_list: None,
};
match scan_type {
fidl_common::ScanType::Active => ScanRequest {
scan_type: fidl_mlme::ScanTypes::Active,
probe_delay: ACTIVE_SCAN_PROBE_DELAY_MS,
min_channel_time: ACTIVE_SCAN_CHANNEL_MS,
max_channel_time: ACTIVE_SCAN_CHANNEL_MS,
..scan_req
},
fidl_common::ScanType::Passive => scan_req,
}
}
fn new_join_scan_request<T>(
mlme_txn_id: u64,
join_scan: &JoinScan<T>,
device_info: &DeviceInfo,
) -> ScanRequest {
new_scan_request(mlme_txn_id, join_scan.scan_type, join_scan.ssid.clone(), device_info)
}
fn new_discovery_scan_request<T>(
mlme_txn_id: u64,
discovery_scan: &DiscoveryScan<T>,
device_info: &DeviceInfo,
) -> ScanRequest {
new_scan_request(mlme_txn_id, discovery_scan.scan_type, vec![], device_info)
}
fn convert_discovery_result(
msg: fidl_mlme::ScanEnd,
bss_list: Vec<BssDescription>,
) -> DiscoveryResult {
match msg.code {
ScanResultCodes::Success => Ok(bss_list),
ScanResultCodes::NotSupported => Err(DiscoveryError::NotSupported),
ScanResultCodes::InvalidArgs => {
error!("Scan returned INVALID_ARGS");
Err(DiscoveryError::InternalError)
}
ScanResultCodes::InternalError => Err(DiscoveryError::InternalError),
}
}
fn get_channels_to_scan(device_info: &DeviceInfo) -> Vec<u8> {
let mut device_supported_channels: HashSet<u8> = HashSet::new();
for band in &device_info.bands {
device_supported_channels.extend(&band.channels);
}
SUPPORTED_CHANNELS
.iter()
.filter(|chan| device_supported_channels.contains(chan))
.map(|chan| *chan)
.collect()
}
const SUPPORTED_CHANNELS: &[u8] = &[
// 5GHz UNII-1
36, 40, 44, 48, // 5GHz UNII-2 Middle
52, 56, 60, 64, // 5GHz UNII-2 Extended
100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, // 5GHz UNII-3
149, 153, 157, 161, 165, // 2GHz
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
];
#[cfg(test)]
mod tests {
use super::*;
use crate::client::test_utils::{fake_bss_with_bssid, fake_unprotected_bss_description};
use crate::clone_utils::clone_bss_desc;
const CLIENT_ADDR: [u8; 6] = [0x7A, 0xE7, 0x76, 0xD9, 0xF2, 0x67];
fn passive_discovery_scan(token: i32) -> DiscoveryScan<i32> {
DiscoveryScan::new(token, fidl_common::ScanType::Passive)
}
#[test]
fn discovery_scan() {
let mut sched = create_sched();
let req = sched
.enqueue_scan_to_discover(passive_discovery_scan(10))
.expect("expected a ScanRequest");
let txn_id = req.txn_id;
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id,
bss: fake_unprotected_bss_description(b"foo".to_vec()),
});
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id: txn_id + 100, // mismatching transaction id
bss: fake_unprotected_bss_description(b"bar".to_vec()),
});
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id,
bss: fake_unprotected_bss_description(b"qux".to_vec()),
});
let (result, req) = sched.on_mlme_scan_end(fidl_mlme::ScanEnd {
txn_id,
code: fidl_mlme::ScanResultCodes::Success,
});
assert!(req.is_none());
let result = match result {
ScanResult::DiscoveryFinished { tokens, result } => {
assert_eq!(vec![10], tokens);
result
}
_ => panic!("expected ScanResult::DiscoveryFinished"),
};
let mut ssid_list = result
.expect("expected a successful scan result")
.into_iter()
.map(|bss| bss.ssid)
.collect::<Vec<_>>();
ssid_list.sort();
assert_eq!(vec![b"foo".to_vec(), b"qux".to_vec()], ssid_list);
}
#[test]
fn test_passive_discovery_scan_args() {
let mut sched = create_sched();
let req = sched
.enqueue_scan_to_discover(passive_discovery_scan(10))
.expect("expected a ScanRequest");
assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Passive);
assert_eq!(req.ssid, vec![]);
}
#[test]
fn test_active_discovery_scan_args() {
let mut sched = create_sched();
let scan_cmd = DiscoveryScan::new(10, fidl_common::ScanType::Active);
let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Active);
assert_eq!(req.ssid, vec![]);
}
#[test]
fn test_discovery_scans_dedupe_single_group() {
let mut sched = create_sched();
// Post one scan command, expect a message to MLME
let mlme_req = sched
.enqueue_scan_to_discover(passive_discovery_scan(10))
.expect("expected a ScanRequest");
let txn_id = mlme_req.txn_id;
// Report a scan result
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id,
bss: fake_unprotected_bss_description(b"foo".to_vec()),
});
// Post another command. It should not issue another request to the MLME since
// there is already an on-going one
assert!(sched.enqueue_scan_to_discover(passive_discovery_scan(20)).is_none());
// Report another scan result and the end of the scan transaction
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id,
bss: fake_unprotected_bss_description(b"bar".to_vec()),
});
let (result, req) = sched.on_mlme_scan_end(fidl_mlme::ScanEnd {
txn_id,
code: fidl_mlme::ScanResultCodes::Success,
});
// We don't expect another request to the MLME
assert!(req.is_none());
// Expect a discovery result with both tokens and both SSIDs
assert_discovery_scan_result(result, vec![10, 20], vec![b"bar".to_vec(), b"foo".to_vec()]);
}
#[test]
fn test_discovery_scans_dedupe_multiple_groups() {
let mut sched = create_sched();
// Post a passive scan command, expect a message to MLME
let mlme_req = sched
.enqueue_scan_to_discover(passive_discovery_scan(10))
.expect("expected a ScanRequest");
let txn_id = mlme_req.txn_id;
// Post an active scan command, which should be enqueued until the previous one finishes
let scan_cmd = DiscoveryScan::new(20, fidl_common::ScanType::Active);
assert!(sched.enqueue_scan_to_discover(scan_cmd).is_none());
// Post a passive scan command. It should be merged with the ongoing one and so should not
// issue another request to MLME
assert!(sched.enqueue_scan_to_discover(passive_discovery_scan(30)).is_none());
// Post an active scan command. It should be merged with the active scan command that's
// still enqueued, and so should not issue another request to MLME
let scan_cmd = DiscoveryScan::new(40, fidl_common::ScanType::Active);
assert!(sched.enqueue_scan_to_discover(scan_cmd).is_none());
// Report scan result and scan end
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id,
bss: fake_unprotected_bss_description(b"foo".to_vec()),
});
let (result, mlme_req) = sched.on_mlme_scan_end(fidl_mlme::ScanEnd {
txn_id,
code: fidl_mlme::ScanResultCodes::Success,
});
// Expect discovery result with 1st and 3rd tokens
assert_discovery_scan_result(result, vec![10, 30], vec![b"foo".to_vec()]);
// Next mlme_req should be an active scan request
assert!(mlme_req.is_some());
let mlme_req = mlme_req.unwrap();
assert_eq!(mlme_req.scan_type, fidl_mlme::ScanTypes::Active);
let txn_id = mlme_req.txn_id;
// Report scan result and scan end
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id,
bss: fake_unprotected_bss_description(b"bar".to_vec()),
});
let (result, mlme_req) = sched.on_mlme_scan_end(fidl_mlme::ScanEnd {
txn_id,
code: fidl_mlme::ScanResultCodes::Success,
});
// Expect discovery result with 2nd and 3rd tokens
assert_discovery_scan_result(result, vec![20, 40], vec![b"bar".to_vec()]);
// We don't expect another request to the MLME
assert!(mlme_req.is_none());
}
fn assert_discovery_scan_result(
result: ScanResult<i32, i32>,
expected_tokens: Vec<i32>,
expected_ssids: Vec<Vec<u8>>,
) {
let result = match result {
ScanResult::DiscoveryFinished { tokens, result } => {
assert_eq!(tokens, expected_tokens);
result
}
_ => panic!("expected ScanResult::DiscoveryFinished"),
};
let mut ssid_list = result
.expect("expected a successful scan result")
.into_iter()
.map(|bss| bss.ssid)
.collect::<Vec<_>>();
ssid_list.sort();
assert_eq!(ssid_list, expected_ssids);
}
#[test]
fn join_scan() {
let mut sched = create_sched();
let (discarded_token, req) = sched.enqueue_scan_to_join(JoinScan {
ssid: b"foo".to_vec(),
token: 10,
scan_type: fidl_common::ScanType::Passive,
});
assert!(discarded_token.is_none());
let txn_id = req.expect("expected a ScanRequest").txn_id;
// Matching BSS
let bss1 = fake_bss_with_bssid(b"foo".to_vec(), [1, 1, 1, 1, 1, 1]);
sched.on_mlme_scan_result(fidl_mlme::ScanResult { txn_id, bss: clone_bss_desc(&bss1) });
// Mismatching transaction ID
let bss2 = fake_bss_with_bssid(b"foo".to_vec(), [2, 2, 2, 2, 2, 2]);
sched.on_mlme_scan_result(fidl_mlme::ScanResult { txn_id: txn_id + 100, bss: bss2 });
// Mismatching SSID
let bss3 = fake_bss_with_bssid(b"bar".to_vec(), [3, 3, 3, 3, 3, 3]);
sched.on_mlme_scan_result(fidl_mlme::ScanResult { txn_id, bss: bss3 });
// Matching BSS
let bss4 = fake_bss_with_bssid(b"foo".to_vec(), [4, 4, 4, 4, 4, 4]);
sched.on_mlme_scan_result(fidl_mlme::ScanResult { txn_id, bss: clone_bss_desc(&bss4) });
let (result, req) = sched.on_mlme_scan_end(fidl_mlme::ScanEnd {
txn_id,
code: fidl_mlme::ScanResultCodes::Success,
});
assert!(req.is_none());
match result {
ScanResult::JoinScanFinished { token, result } => {
assert_eq!(10, token);
assert_eq!(result, Ok(vec![bss1, bss4]));
}
_ => panic!("expected ScanResult::ReadyToJoin"),
};
}
fn passive_join_scan(ssid: Vec<u8>, token: i32) -> JoinScan<i32> {
JoinScan { ssid, token, scan_type: fidl_common::ScanType::Passive }
}
#[test]
fn test_passive_join_scan_args() {
let mut sched = create_sched();
let (_discarded_token, req) =
sched.enqueue_scan_to_join(passive_join_scan(b"foo".to_vec(), 10));
let req = req.expect("expected a ScanRequest");
assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Passive);
assert_eq!(req.ssid, b"foo".to_vec());
}
#[test]
fn test_active_join_scan_args() {
let mut sched = create_sched();
let (_discarded_token, req) = sched.enqueue_scan_to_join(JoinScan {
ssid: b"foo".to_vec(),
token: 10,
scan_type: fidl_common::ScanType::Active,
});
let req = req.expect("expected a ScanRequest");
assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Active);
assert_eq!(req.ssid, b"foo".to_vec());
}
#[test]
fn get_join_scan() {
let mut sched = create_sched();
assert_eq!(None, sched.get_join_scan());
sched.enqueue_scan_to_join(passive_join_scan(b"foo".to_vec(), 10));
// Make sure the scanner is in the state we expect it to be: the request is
// 'current', not 'pending'
assert_eq!(
ScanState::ScanningToJoin {
cmd: passive_join_scan(b"foo".to_vec(), 10),
mlme_txn_id: 1,
bss_list: vec![],
},
sched.current
);
assert_eq!(None, sched.pending_join);
assert_eq!(Some(&passive_join_scan(b"foo".to_vec(), 10)), sched.get_join_scan());
sched.enqueue_scan_to_join(passive_join_scan(b"bar".to_vec(), 20));
// Again, make sure the state is what we expect. "Foo" should still be the current request,
// while "bar" should be pending
assert_eq!(
ScanState::ScanningToJoin {
cmd: passive_join_scan(b"foo".to_vec(), 10),
mlme_txn_id: 1,
bss_list: vec![],
},
sched.current
);
assert_eq!(Some(passive_join_scan(b"bar".to_vec(), 20)), sched.pending_join);
// Expect the pending request to be returned since the current one will be discarded
// once the scan finishes
assert_eq!(Some(&passive_join_scan(b"bar".to_vec(), 20)), sched.get_join_scan());
}
fn create_sched() -> ScanScheduler<i32, i32> {
ScanScheduler::new(Arc::new(DeviceInfo { addr: CLIENT_ADDR, bands: vec![] }))
}
}