wlan_common/
scan.rs

1// Copyright 2021 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::bss::BssDescription;
6use crate::mac::MacRole;
7use crate::security::SecurityDescriptor;
8use anyhow::format_err;
9use fidl_fuchsia_wlan_sme as fidl_sme;
10use std::borrow::Cow;
11use std::collections::{HashMap, HashSet};
12use std::error;
13use std::fmt::{self, Display, Formatter};
14
15#[cfg(target_os = "fuchsia")]
16use anyhow::Context as _;
17
18/// The compatibility of a BSS with respect to a scanning interface.
19///
20/// Describes the possible configurations for connection to a compatible BSS or disjoint features
21/// that prevent a connection to an incompatible BSS. Here, _compatibility_ refers to the ability
22/// to establish a connection.
23///
24/// When compatibility is `Err` for a BSS, then the scanning interface cannot establish a
25/// connection.
26pub type Compatibility = Result<Compatible, Incompatible>;
27
28pub trait CompatibilityExt: Sized {
29    fn try_from_fidl(
30        compatibility: fidl_sme::Compatibility,
31    ) -> Result<Self, fidl_sme::Compatibility>;
32
33    fn into_fidl(self) -> fidl_sme::Compatibility;
34}
35
36impl CompatibilityExt for Compatibility {
37    fn try_from_fidl(
38        compatibility: fidl_sme::Compatibility,
39    ) -> Result<Self, fidl_sme::Compatibility> {
40        match compatibility {
41            fidl_sme::Compatibility::Compatible(compatible) => Compatible::try_from(compatible)
42                .map(Ok)
43                .map_err(fidl_sme::Compatibility::Compatible),
44            fidl_sme::Compatibility::Incompatible(incompatible) => {
45                Incompatible::try_from(incompatible)
46                    .map(Err)
47                    .map_err(fidl_sme::Compatibility::Incompatible)
48            }
49        }
50    }
51
52    fn into_fidl(self) -> fidl_sme::Compatibility {
53        match self {
54            Ok(compatible) => fidl_sme::Compatibility::Compatible(compatible.into()),
55            Err(incompatible) => fidl_sme::Compatibility::Incompatible(incompatible.into()),
56        }
57    }
58}
59
60/// Supported configurations for a compatible BSS with respect to a scanning interface.
61///
62/// Describes the mutually supported features between a compatible BSS and a local scanning
63/// interface that can be negotiated and/or used to establish a connection.
64#[derive(Debug, Clone, PartialEq)]
65pub struct Compatible {
66    mutual_security_protocols: HashSet<SecurityDescriptor>,
67}
68
69impl Compatible {
70    /// Constructs a `Compatible` from mutually supported features.
71    ///
72    /// Returns `None` if any set of mutually supported features is empty, because this implies
73    /// incompatibility.
74    ///
75    /// Note that the features considered by `Compatible` depend on the needs of downstream code
76    /// and may change. This function accepts parameters that represent only these features, which
77    /// may be as few in number as one and may grow to many.
78    pub fn try_from_features(
79        mutual_security_protocols: impl IntoIterator<Item = SecurityDescriptor>,
80    ) -> Option<Self> {
81        let mutual_security_protocols: HashSet<_> = mutual_security_protocols.into_iter().collect();
82        if mutual_security_protocols.is_empty() {
83            None
84        } else {
85            Some(Compatible { mutual_security_protocols })
86        }
87    }
88
89    /// Constructs a [`Compatibility`] from a `Compatible` from mutually supported features.
90    ///
91    /// While this function presents a fallible interface and returns a `Compatibility` (`Result`),
92    /// it panics on failure and never returns `Err`. This can be used when a `Compatibility` is
93    /// needed but it is important to assert that it is compatible (`Ok`), most notably in tests.
94    ///
95    /// See [`Compatible::try_from_features`].
96    ///
97    /// # Panics
98    ///
99    /// Panics if a `Compatible` cannot be constructed from the given mutually supported features.
100    /// This occurs if `Compatible::try_from_features` returns `None`.
101    pub fn expect_ok(
102        mutual_security_protocols: impl IntoIterator<Item = SecurityDescriptor>,
103    ) -> Compatibility {
104        match Compatible::try_from_features(mutual_security_protocols) {
105            Some(compatibility) => Ok(compatibility),
106            None => panic!("mutual modes of operation are absent and imply incompatiblity"),
107        }
108    }
109
110    /// Gets the set of mutually supported security protocols.
111    ///
112    /// This set represents the intersection of security protocols supported by the BSS and the
113    /// scanning interface. In this context, this set is never empty, as that would imply
114    /// incompatibility.
115    pub fn mutual_security_protocols(&self) -> &HashSet<SecurityDescriptor> {
116        &self.mutual_security_protocols
117    }
118}
119
120impl From<Compatible> for fidl_sme::Compatible {
121    fn from(compatibility: Compatible) -> Self {
122        let Compatible { mutual_security_protocols } = compatibility;
123        fidl_sme::Compatible {
124            mutual_security_protocols: mutual_security_protocols
125                .into_iter()
126                .map(From::from)
127                .collect(),
128        }
129    }
130}
131
132impl From<Compatible> for HashSet<SecurityDescriptor> {
133    fn from(compatibility: Compatible) -> Self {
134        compatibility.mutual_security_protocols
135    }
136}
137
138impl TryFrom<fidl_sme::Compatible> for Compatible {
139    type Error = fidl_sme::Compatible;
140
141    fn try_from(compatibility: fidl_sme::Compatible) -> Result<Self, Self::Error> {
142        let fidl_sme::Compatible { mutual_security_protocols } = compatibility;
143        match Compatible::try_from_features(
144            mutual_security_protocols.iter().cloned().map(From::from),
145        ) {
146            Some(compatible) => Ok(compatible),
147            None => Err(fidl_sme::Compatible { mutual_security_protocols }),
148        }
149    }
150}
151
152// TODO(https://fxbug.dev/384797729): Consider supported channels and data rates.
153/// Unsupported configurations for an incompatible BSS with respect to a scanning interface.
154///
155/// Describes disjoint features between an incompatible BSS and a local scanning interface that
156/// prevent establishing a connection. Information about modes of operation is best effort;
157/// `Incompatible` may provide no additional information at all.
158#[derive(Debug, Clone, PartialEq)]
159pub struct Incompatible {
160    description: Cow<'static, str>,
161    disjoint_security_protocols: Option<HashMap<SecurityDescriptor, MacRole>>,
162}
163
164impl Incompatible {
165    /// Constructs an `Incompatible` from a description with no feature information.
166    pub fn from_description(description: impl Into<Cow<'static, str>>) -> Self {
167        Incompatible { description: description.into(), disjoint_security_protocols: None }
168    }
169
170    /// Constructs an `Incompatible` from a description and disjoint features.
171    ///
172    /// Returns `None` if any given features are **not** disjoint. For example, `None` is returned
173    /// if a security protocol appears more than once with differing roles, because this implies
174    /// compatibility (a mutually supported security protocol).
175    ///
176    /// Note that the features considered by `Incompatible` depend on the needs of downstream code
177    /// and may change. This function accepts parameters that represent only these features, which
178    /// may be as few in number as one and may grow to many.
179    pub fn try_from_features(
180        description: impl Into<Cow<'static, str>>,
181        disjoint_security_protocols: Option<
182            impl IntoIterator<Item = (SecurityDescriptor, MacRole)>,
183        >,
184    ) -> Option<Self> {
185        disjoint_security_protocols
186            .map(|disjoint_security_protocols| {
187                let mut unique_security_protocols = HashMap::new();
188                for (descriptor, role) in disjoint_security_protocols {
189                    if let Some(previous) = unique_security_protocols.insert(descriptor, role) {
190                        if role != previous {
191                            return Err(role);
192                        }
193                    }
194                }
195                Ok(unique_security_protocols)
196            })
197            .transpose()
198            .ok()
199            .map(move |disjoint_security_protocols| Incompatible {
200                description: description.into(),
201                disjoint_security_protocols,
202            })
203    }
204
205    /// Constructs a [`Compatibility`] from an `Incompatible` with no feature information.
206    pub const fn unknown() -> Compatibility {
207        Err(Incompatible {
208            description: Cow::Borrowed("unknown"),
209            disjoint_security_protocols: None,
210        })
211    }
212
213    /// Constructs a [`Compatibility`] from an `Incompatible` from disjoint features.
214    ///
215    /// While this function presents a fallible interface and returns a `Compatibility` (`Result`),
216    /// it panics on failure and never returns `Ok`. This can be used when a `Compatibility` is
217    /// needed but it is important to assert that it is incompatible (`Err`), most notably in tests.
218    ///
219    /// See [`Incompatible::try_from_features`].
220    ///
221    /// # Panics
222    ///
223    /// Panics if an `Incompatible` cannot be constructed from the given disjoint features. This
224    /// occurs if `Incompatible::try_from_features` returns `None`.
225    pub fn expect_err(
226        description: impl Into<Cow<'static, str>>,
227        disjoint_security_protocols: Option<
228            impl IntoIterator<Item = (SecurityDescriptor, MacRole)>,
229        >,
230    ) -> Compatibility {
231        match Incompatible::try_from_features(description, disjoint_security_protocols) {
232            Some(incompatible) => Err(incompatible),
233            None => panic!("disjoint modes of operation intersect and imply compatiblity"),
234        }
235    }
236
237    /// Gets the sets of disjoint security protocols, if any.
238    ///
239    /// The disjoint sets are represented as a map from `SecurityDescriptor` to `MacRole`, where
240    /// each security protocol is supported only by one station in a particular role (e.g., client
241    /// and AP). There is a disjoint set of security protocols for each unique role in the map.
242    ///
243    /// Returns `None` if no security protocol incompatibility has been detected. When `Some` but
244    /// with an empty map, security protocol support is considered incompatible even though the
245    /// protocols are not described.
246    pub fn disjoint_security_protocols(&self) -> Option<&HashMap<SecurityDescriptor, MacRole>> {
247        self.disjoint_security_protocols.as_ref()
248    }
249}
250
251impl Display for Incompatible {
252    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
253        write!(formatter, "incompatibility detected")?;
254        if !self.description.is_empty() {
255            write!(formatter, ": {}", self.description)?;
256        }
257        if let Some(disjoint_security_protocols) = self.disjoint_security_protocols() {
258            let (client_security_protocols, bss_security_protocols) = disjoint_security_protocols
259                .iter()
260                .partition::<Vec<_>, _>(|(_, role)| matches!(role, MacRole::Client));
261            write!(
262                formatter,
263                ": supported BSS vs. client security protocols: {:?} vs. {:?}",
264                bss_security_protocols, client_security_protocols,
265            )?;
266        }
267        write!(formatter, ".")
268    }
269}
270
271impl error::Error for Incompatible {}
272
273impl From<Incompatible> for fidl_sme::Incompatible {
274    fn from(incompatible: Incompatible) -> Self {
275        let Incompatible { description, disjoint_security_protocols } = incompatible;
276        fidl_sme::Incompatible {
277            description: description.into(),
278            disjoint_security_protocols: disjoint_security_protocols.map(
279                |disjoint_security_protocols| {
280                    disjoint_security_protocols
281                        .into_iter()
282                        .map(|(security_protocol, role)| fidl_sme::DisjointSecurityProtocol {
283                            protocol: security_protocol.into(),
284                            role: role.into(),
285                        })
286                        .collect()
287                },
288            ),
289        }
290    }
291}
292
293impl TryFrom<fidl_sme::Incompatible> for Incompatible {
294    type Error = fidl_sme::Incompatible;
295
296    fn try_from(incompatible: fidl_sme::Incompatible) -> Result<Self, Self::Error> {
297        let fidl_sme::Incompatible { description, disjoint_security_protocols } = incompatible;
298        match disjoint_security_protocols
299            .as_ref()
300            .map(|disjoint_security_protocols| {
301                disjoint_security_protocols
302                    .iter()
303                    .copied()
304                    .map(|fidl_sme::DisjointSecurityProtocol { protocol, role }| {
305                        MacRole::try_from(role).map(|role| (protocol.into(), role))
306                    })
307                    .collect::<Result<Vec<_>, _>>()
308            })
309            .transpose()
310        {
311            Ok(converted_security_protocols) => {
312                Incompatible::try_from_features(description.clone(), converted_security_protocols)
313                    .ok_or(fidl_sme::Incompatible { description, disjoint_security_protocols })
314            }
315            Err(_) => Err(fidl_sme::Incompatible { description, disjoint_security_protocols }),
316        }
317    }
318}
319
320#[derive(Debug, Clone, PartialEq)]
321pub struct ScanResult {
322    pub compatibility: Compatibility,
323    // Time of the scan result relative to when the system was powered on.
324    // See https://fuchsia.dev/fuchsia-src/concepts/time/language_support?hl=en#monotonic_time
325    #[cfg(target_os = "fuchsia")]
326    pub timestamp: zx::MonotonicInstant,
327    pub bss_description: BssDescription,
328}
329
330impl ScanResult {
331    pub fn is_compatible(&self) -> bool {
332        self.compatibility.is_ok()
333    }
334}
335
336impl From<ScanResult> for fidl_sme::ScanResult {
337    fn from(scan_result: ScanResult) -> fidl_sme::ScanResult {
338        let ScanResult {
339            compatibility,
340            #[cfg(target_os = "fuchsia")]
341            timestamp,
342            bss_description,
343        } = scan_result;
344        fidl_sme::ScanResult {
345            compatibility: compatibility.into_fidl(),
346            #[cfg(target_os = "fuchsia")]
347            timestamp_nanos: timestamp.into_nanos(),
348            #[cfg(not(target_os = "fuchsia"))]
349            timestamp_nanos: 0,
350            bss_description: bss_description.into(),
351        }
352    }
353}
354
355impl TryFrom<fidl_sme::ScanResult> for ScanResult {
356    type Error = anyhow::Error;
357
358    fn try_from(scan_result: fidl_sme::ScanResult) -> Result<ScanResult, Self::Error> {
359        let fidl_sme::ScanResult { compatibility, timestamp_nanos, bss_description } = scan_result;
360        Ok(ScanResult {
361            compatibility: Compatibility::try_from_fidl(compatibility)
362                .map_err(|_| format_err!("failed to convert FIDL `Compatibility`"))?,
363            #[cfg(target_os = "fuchsia")]
364            timestamp: zx::MonotonicInstant::from_nanos(timestamp_nanos),
365            bss_description: bss_description.try_into()?,
366        })
367    }
368}
369
370/// Creates a VMO containing FIDL-encoded scan results.
371#[cfg(target_os = "fuchsia")]
372pub fn write_vmo(results: Vec<fidl_sme::ScanResult>) -> Result<fidl::Vmo, anyhow::Error> {
373    let bytes =
374        fidl::persist(&fidl_sme::ScanResultVector { results }).context("encoding scan results")?;
375    let vmo = fidl::Vmo::create(bytes.len() as u64).context("creating VMO for scan results")?;
376    vmo.write(&bytes, 0).context("writing scan results to VMO")?;
377    Ok(vmo)
378}
379
380/// Reads FIDL-encoded scan results from a VMO.
381#[cfg(target_os = "fuchsia")]
382pub fn read_vmo(vmo: fidl::Vmo) -> Result<Vec<fidl_sme::ScanResult>, anyhow::Error> {
383    let size = vmo.get_content_size().context("getting VMO content size")?;
384    let bytes = vmo.read_to_vec(0, size).context("reading VMO of scan results")?;
385    let scan_result_vector =
386        fidl::unpersist::<fidl_sme::ScanResultVector>(&bytes).context("decoding scan results")?;
387    Ok(scan_result_vector.results)
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn compatible_from_only_empty_is_none() {
396        assert!(Compatible::try_from_features([]).is_none());
397    }
398
399    #[test]
400    fn compatible_from_mutual_security_protocols_is_some() {
401        assert!(Compatible::try_from_features([
402            SecurityDescriptor::WPA2_PERSONAL,
403            SecurityDescriptor::WPA3_PERSONAL,
404        ])
405        .is_some());
406    }
407
408    #[test]
409    fn incompatible_from_only_none_is_some() {
410        assert!(Incompatible::try_from_features(
411            "dunno",
412            None::<[(SecurityDescriptor, MacRole); 0]>
413        )
414        .is_some());
415    }
416
417    #[test]
418    fn incompatible_from_only_some_empty_is_some() {
419        assert!(Incompatible::try_from_features("dunno", Some([])).is_some());
420    }
421
422    #[test]
423    fn incompatible_from_disjoint_security_protocols_is_some() {
424        assert!(Incompatible::try_from_features(
425            "dunno",
426            Some([
427                (SecurityDescriptor::WPA2_PERSONAL, MacRole::Client),
428                (SecurityDescriptor::WPA3_PERSONAL, MacRole::Ap),
429            ])
430        )
431        .is_some());
432    }
433
434    #[test]
435    fn incompatible_from_mutual_security_protocols_is_none() {
436        assert!(Incompatible::try_from_features(
437            "dunno",
438            Some([
439                (SecurityDescriptor::WPA3_PERSONAL, MacRole::Client),
440                (SecurityDescriptor::WPA3_PERSONAL, MacRole::Ap),
441            ])
442        )
443        .is_none());
444    }
445
446    #[test]
447    fn fidl_from_compatible_eq_expected() {
448        let security_protocol = SecurityDescriptor::OPEN;
449        let fidl =
450            fidl_sme::Compatible::from(Compatible::try_from_features([security_protocol]).unwrap());
451        assert_eq!(
452            fidl,
453            fidl_sme::Compatible { mutual_security_protocols: vec![security_protocol.into()] },
454        );
455    }
456
457    #[test]
458    fn compatible_try_from_fidl_eq_ok() {
459        let security_protocol = SecurityDescriptor::OPEN;
460        let compatible = Compatible::try_from(fidl_sme::Compatible {
461            mutual_security_protocols: vec![security_protocol.into()],
462        });
463        assert_eq!(compatible, Ok(Compatible::try_from_features([security_protocol]).unwrap()));
464    }
465
466    #[test]
467    fn compatible_try_from_fidl_eq_err() {
468        let fidl = fidl_sme::Compatible { mutual_security_protocols: vec![] };
469        let compatible = Compatible::try_from(fidl.clone());
470        assert_eq!(compatible, Err(fidl));
471    }
472
473    #[test]
474    fn fidl_from_incompatible_eq_expected() {
475        let fidl = fidl_sme::Incompatible::from(
476            Incompatible::try_from_features(
477                "dunno",
478                // Only one protocol-role entry is used here, because entries are stored in a
479                // `HashMap` and ordering is arbitrary when this is converted into a `Vec` in the
480                // FIDL representation. This causes spurious errors, since `[a, b]` does not equal
481                // `[b, a]`, though they are semantically equivalent here.
482                Some([(SecurityDescriptor::WPA3_PERSONAL, MacRole::Ap)]),
483            )
484            .unwrap(),
485        );
486        assert_eq!(
487            fidl,
488            fidl_sme::Incompatible {
489                description: String::from("dunno"),
490                disjoint_security_protocols: Some(vec![fidl_sme::DisjointSecurityProtocol {
491                    protocol: SecurityDescriptor::WPA3_PERSONAL.into(),
492                    role: MacRole::Ap.into(),
493                },]),
494            },
495        );
496    }
497
498    #[test]
499    fn incompatible_try_from_fidl_eq_expected() {
500        let incompatible = Incompatible::try_from(fidl_sme::Incompatible {
501            description: String::from("dunno"),
502            disjoint_security_protocols: Some(vec![
503                fidl_sme::DisjointSecurityProtocol {
504                    protocol: SecurityDescriptor::WPA2_PERSONAL.into(),
505                    role: MacRole::Client.into(),
506                },
507                fidl_sme::DisjointSecurityProtocol {
508                    protocol: SecurityDescriptor::WPA3_PERSONAL.into(),
509                    role: MacRole::Ap.into(),
510                },
511            ]),
512        });
513        assert_eq!(
514            incompatible,
515            Ok(Incompatible::try_from_features(
516                "dunno",
517                Some([
518                    (SecurityDescriptor::WPA2_PERSONAL, MacRole::Client),
519                    (SecurityDescriptor::WPA3_PERSONAL, MacRole::Ap),
520                ])
521            )
522            .unwrap()),
523        );
524    }
525
526    #[test]
527    fn fidl_from_compatibility_eq_expected() {
528        let security_protocol = SecurityDescriptor::OPEN;
529        let fidl = Compatible::expect_ok([security_protocol]).into_fidl();
530        assert_eq!(
531            fidl,
532            fidl_sme::Compatibility::Compatible(fidl_sme::Compatible {
533                mutual_security_protocols: vec![security_protocol.into()],
534            }),
535        );
536    }
537
538    #[test]
539    fn compatibility_try_from_fidl_eq_ok() {
540        let security_protocol = SecurityDescriptor::OPEN;
541        let compatibility = Compatibility::try_from_fidl(fidl_sme::Compatibility::Compatible(
542            fidl_sme::Compatible { mutual_security_protocols: vec![security_protocol.into()] },
543        ));
544        assert_eq!(compatibility, Ok(Compatible::expect_ok([security_protocol])));
545    }
546
547    #[test]
548    fn compatibility_try_from_fidl_eq_err() {
549        let fidl = fidl_sme::Compatibility::Compatible(fidl_sme::Compatible {
550            mutual_security_protocols: vec![],
551        });
552        let compatibility = Compatibility::try_from_fidl(fidl.clone());
553        assert_eq!(compatibility, Err(fidl));
554    }
555}