netstack3_base/
matchers.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
5//! Trait definition for matchers.
6
7use alloc::format;
8use alloc::string::String;
9use core::fmt::Debug;
10use core::num::NonZeroU64;
11
12use derivative::Derivative;
13use net_types::ip::{IpAddress, Subnet};
14
15use crate::{InspectableValue, Inspector};
16
17/// Trait defining required types for matchers provided by bindings.
18///
19/// Allows rules that match on device class to be installed, storing the
20/// [`MatcherBindingsTypes::DeviceClass`] type at rest, while allowing Netstack3
21/// Core to have Bindings provide the type since it is platform-specific.
22pub trait MatcherBindingsTypes {
23    /// The device class type for devices installed in the netstack.
24    type DeviceClass: Clone + Debug;
25}
26
27/// Common pattern to define a matcher for a metadata input `T`.
28///
29/// Used in matching engines like filtering and routing rules.
30pub trait Matcher<T> {
31    /// Returns whether the provided value matches.
32    fn matches(&self, actual: &T) -> bool;
33
34    /// Returns whether the provided value is set and matches.
35    fn required_matches(&self, actual: Option<&T>) -> bool {
36        actual.map_or(false, |actual| self.matches(actual))
37    }
38}
39
40/// Implement `Matcher` for optional matchers, so that if a matcher is left
41/// unspecified, it matches all inputs by default.
42impl<T, O> Matcher<T> for Option<O>
43where
44    O: Matcher<T>,
45{
46    fn matches(&self, actual: &T) -> bool {
47        self.as_ref().map_or(true, |expected| expected.matches(actual))
48    }
49
50    fn required_matches(&self, actual: Option<&T>) -> bool {
51        self.as_ref().map_or(true, |expected| expected.required_matches(actual))
52    }
53}
54
55/// Matcher that matches IP addresses in a subnet.
56#[derive(Debug, Copy, Clone, PartialEq, Eq)]
57pub struct SubnetMatcher<A: IpAddress>(pub Subnet<A>);
58
59impl<A: IpAddress> Matcher<A> for SubnetMatcher<A> {
60    fn matches(&self, actual: &A) -> bool {
61        let Self(matcher) = self;
62        matcher.contains(actual)
63    }
64}
65
66/// A matcher for network interfaces.
67#[derive(Clone, Derivative, PartialEq, Eq)]
68#[derivative(Debug)]
69pub enum InterfaceMatcher<DeviceClass> {
70    /// The ID of the interface as assigned by the netstack.
71    Id(NonZeroU64),
72    /// Match based on name.
73    Name(String),
74    /// The device class of the interface.
75    DeviceClass(DeviceClass),
76}
77
78impl<DeviceClass: Debug> InspectableValue for InterfaceMatcher<DeviceClass> {
79    fn record<I: Inspector>(&self, name: &str, inspector: &mut I) {
80        match self {
81            InterfaceMatcher::Id(id) => inspector.record_string(name, format!("Id({})", id.get())),
82            InterfaceMatcher::Name(iface_name) => {
83                inspector.record_string(name, format!("Name({iface_name})"))
84            }
85            InterfaceMatcher::DeviceClass(class) => {
86                inspector.record_debug(name, format!("Class({class:?})"))
87            }
88        };
89    }
90}
91
92/// Allows code to match on properties of an interface (ID, name, and device
93/// class) without Netstack3 Core (or Bindings, in the case of the device class)
94/// having to specifically expose that state.
95pub trait InterfaceProperties<DeviceClass> {
96    /// Returns whether the provided ID matches the interface.
97    fn id_matches(&self, id: &NonZeroU64) -> bool;
98
99    /// Returns whether the provided name matches the interface.
100    fn name_matches(&self, name: &str) -> bool;
101
102    /// Returns whether the provided device class matches the interface.
103    fn device_class_matches(&self, device_class: &DeviceClass) -> bool;
104}
105
106impl<DeviceClass, I: InterfaceProperties<DeviceClass>> Matcher<I>
107    for InterfaceMatcher<DeviceClass>
108{
109    fn matches(&self, actual: &I) -> bool {
110        match self {
111            InterfaceMatcher::Id(id) => actual.id_matches(id),
112            InterfaceMatcher::Name(name) => actual.name_matches(name),
113            InterfaceMatcher::DeviceClass(device_class) => {
114                actual.device_class_matches(device_class)
115            }
116        }
117    }
118}
119
120/// Matcher for the bound device of locally generated traffic.
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub enum BoundInterfaceMatcher<DeviceClass> {
123    /// The packet is bound to a device which is matched by the matcher.
124    Bound(InterfaceMatcher<DeviceClass>),
125    /// There is no bound device.
126    Unbound,
127}
128
129impl<'a, DeviceClass, D: InterfaceProperties<DeviceClass>> Matcher<Option<&'a D>>
130    for BoundInterfaceMatcher<DeviceClass>
131{
132    fn matches(&self, actual: &Option<&'a D>) -> bool {
133        match self {
134            BoundInterfaceMatcher::Bound(matcher) => matcher.required_matches(actual.as_deref()),
135            BoundInterfaceMatcher::Unbound => actual.is_none(),
136        }
137    }
138}
139
140impl<DeviceClass: Debug> InspectableValue for BoundInterfaceMatcher<DeviceClass> {
141    fn record<I: Inspector>(&self, name: &str, inspector: &mut I) {
142        match self {
143            BoundInterfaceMatcher::Unbound => inspector.record_str(name, "Unbound"),
144            BoundInterfaceMatcher::Bound(interface) => {
145                inspector.record_inspectable_value(name, interface)
146            }
147        }
148    }
149}
150
151#[cfg(any(test, feature = "testutils"))]
152pub(crate) mod testutil {
153    use alloc::string::String;
154    use core::num::NonZeroU64;
155
156    use crate::matchers::InterfaceProperties;
157    use crate::testutil::{FakeDeviceClass, FakeStrongDeviceId, FakeWeakDeviceId};
158    use crate::{DeviceIdentifier, StrongDeviceIdentifier};
159
160    /// A fake device ID for testing matchers.
161    #[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
162    #[allow(missing_docs)]
163    pub struct FakeMatcherDeviceId {
164        pub id: NonZeroU64,
165        pub name: String,
166        pub class: FakeDeviceClass,
167    }
168
169    #[allow(missing_docs)]
170    impl FakeMatcherDeviceId {
171        pub fn wlan_interface() -> FakeMatcherDeviceId {
172            FakeMatcherDeviceId {
173                id: NonZeroU64::new(1).unwrap(),
174                name: String::from("wlan"),
175                class: FakeDeviceClass::Wlan,
176            }
177        }
178
179        pub fn ethernet_interface() -> FakeMatcherDeviceId {
180            FakeMatcherDeviceId {
181                id: NonZeroU64::new(2).unwrap(),
182                name: String::from("eth"),
183                class: FakeDeviceClass::Ethernet,
184            }
185        }
186    }
187
188    impl StrongDeviceIdentifier for FakeMatcherDeviceId {
189        type Weak = FakeWeakDeviceId<Self>;
190
191        fn downgrade(&self) -> Self::Weak {
192            FakeWeakDeviceId(self.clone())
193        }
194    }
195
196    impl DeviceIdentifier for FakeMatcherDeviceId {
197        fn is_loopback(&self) -> bool {
198            false
199        }
200    }
201
202    impl FakeStrongDeviceId for FakeMatcherDeviceId {
203        fn is_alive(&self) -> bool {
204            true
205        }
206    }
207
208    impl PartialEq<FakeWeakDeviceId<FakeMatcherDeviceId>> for FakeMatcherDeviceId {
209        fn eq(&self, FakeWeakDeviceId(other): &FakeWeakDeviceId<FakeMatcherDeviceId>) -> bool {
210            self == other
211        }
212    }
213
214    impl InterfaceProperties<FakeDeviceClass> for FakeMatcherDeviceId {
215        fn id_matches(&self, id: &NonZeroU64) -> bool {
216            &self.id == id
217        }
218
219        fn name_matches(&self, name: &str) -> bool {
220            &self.name == name
221        }
222
223        fn device_class_matches(&self, class: &FakeDeviceClass) -> bool {
224            &self.class == class
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use alloc::format;
232
233    use ip_test_macro::ip_test;
234    use net_types::ip::Ip;
235
236    use super::*;
237    use crate::testutil::{FakeDeviceId, TestIpExt};
238
239    /// Only matches `true`.
240    #[derive(Debug)]
241    struct TrueMatcher;
242
243    impl Matcher<bool> for TrueMatcher {
244        fn matches(&self, actual: &bool) -> bool {
245            *actual
246        }
247    }
248
249    #[test]
250    fn test_optional_matcher_optional_value() {
251        assert!(TrueMatcher.matches(&true));
252        assert!(!TrueMatcher.matches(&false));
253
254        assert!(TrueMatcher.required_matches(Some(&true)));
255        assert!(!TrueMatcher.required_matches(Some(&false)));
256        assert!(!TrueMatcher.required_matches(None));
257
258        assert!(Some(TrueMatcher).matches(&true));
259        assert!(!Some(TrueMatcher).matches(&false));
260        assert!(None::<TrueMatcher>.matches(&true));
261        assert!(None::<TrueMatcher>.matches(&false));
262
263        assert!(Some(TrueMatcher).required_matches(Some(&true)));
264        assert!(!Some(TrueMatcher).required_matches(Some(&false)));
265        assert!(!Some(TrueMatcher).required_matches(None));
266        assert!(None::<TrueMatcher>.required_matches(Some(&true)));
267        assert!(None::<TrueMatcher>.required_matches(Some(&false)));
268        assert!(None::<TrueMatcher>.required_matches(None));
269    }
270
271    #[test]
272    fn device_name_matcher() {
273        let device = FakeDeviceId;
274        let positive_matcher = InterfaceMatcher::Name(FakeDeviceId::FAKE_NAME.into());
275        let negative_matcher =
276            InterfaceMatcher::Name(format!("DONTMATCH-{}", FakeDeviceId::FAKE_NAME));
277        assert!(positive_matcher.matches(&device));
278        assert!(!negative_matcher.matches(&device));
279    }
280
281    #[ip_test(I)]
282    fn subnet_matcher<I: Ip + TestIpExt>() {
283        let matcher = SubnetMatcher(I::TEST_ADDRS.subnet);
284        assert!(matcher.matches(&I::TEST_ADDRS.local_ip));
285        assert!(!matcher.matches(&I::get_other_remote_ip_address(1)));
286    }
287}