netstack3_ip/routing/
rules.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//! IP routing rules.
6
7use alloc::vec::Vec;
8use core::fmt::Debug;
9use core::ops::Deref as _;
10
11use net_types::ip::Ip;
12use netstack3_base::{
13    BoundInterfaceMatcher, InterfaceProperties, MarkMatchers, Marks, Matcher, MatcherBindingsTypes,
14    SubnetMatcher,
15};
16
17use crate::internal::routing::PacketOrigin;
18use crate::{IpRoutingBindingsTypes, RoutingTableId};
19
20/// Table that contains routing rules.
21pub struct RulesTable<I: Ip, D, BT: IpRoutingBindingsTypes + MatcherBindingsTypes> {
22    /// Rules of the table.
23    rules: Vec<Rule<I, D, BT>>,
24}
25
26impl<I: Ip, D, BT: IpRoutingBindingsTypes + MatcherBindingsTypes> RulesTable<I, D, BT> {
27    pub(crate) fn new(main_table_id: RoutingTableId<I, D, BT>) -> Self {
28        // TODO(https://fxbug.dev/355059790): If bindings is installing the main table, we should
29        // also let the bindings install this default rule.
30        Self {
31            rules: alloc::vec![Rule {
32                matcher: RuleMatcher::match_all_packets(),
33                action: RuleAction::Lookup(main_table_id)
34            }],
35        }
36    }
37
38    pub(crate) fn iter(&self) -> impl Iterator<Item = &'_ Rule<I, D, BT>> {
39        self.rules.iter()
40    }
41
42    /// Gets the mutable reference to the rules vector.
43    #[cfg(any(test, feature = "testutils"))]
44    pub fn rules_mut(&mut self) -> &mut Vec<Rule<I, D, BT>> {
45        &mut self.rules
46    }
47
48    /// Replaces the rules inside this table.
49    pub fn replace(&mut self, new_rules: Vec<Rule<I, D, BT>>) {
50        self.rules = new_rules;
51    }
52}
53
54/// A routing rule.
55pub struct Rule<I: Ip, D, BT: IpRoutingBindingsTypes + MatcherBindingsTypes> {
56    /// The matcher of the rule.
57    pub matcher: RuleMatcher<I, BT::DeviceClass>,
58    /// The action of the rule.
59    pub action: RuleAction<RoutingTableId<I, D, BT>>,
60}
61
62/// The action part of a [`Rule`].
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum RuleAction<Lookup> {
65    /// Will resolve to unreachable.
66    Unreachable,
67    /// Lookup in a routing table.
68    Lookup(Lookup),
69}
70
71/// Matches with [`PacketOrigin`].
72///
73/// Note that this matcher doesn't specify the source address/bound address like [`PacketOrigin`]
74/// because the user can specify a source address matcher without specifying the direction of the
75/// traffic.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum TrafficOriginMatcher<DeviceClass> {
78    /// This only matches packets that are generated locally; the optional interface matcher
79    /// can be used to match what device is bound to by `SO_BINDTODEVICE`.
80    Local {
81        /// The matcher for the bound device.
82        // TODO(https://fxbug.dev/441124570): Support referencey semantics for
83        // ID matchers.
84        bound_device_matcher: Option<BoundInterfaceMatcher<DeviceClass>>,
85    },
86    /// This only matches non-local packets. The packets must be received from the network.
87    NonLocal,
88}
89
90impl<'a, I: Ip, D> Matcher<PacketOrigin<I, &'a D>> for SubnetMatcher<I::Addr> {
91    fn matches(&self, actual: &PacketOrigin<I, &'a D>) -> bool {
92        match actual {
93            PacketOrigin::Local { bound_address, bound_device: _ } => {
94                self.required_matches(bound_address.as_deref())
95            }
96            PacketOrigin::NonLocal { source_address, incoming_device: _ } => {
97                self.matches(source_address.deref())
98            }
99        }
100    }
101}
102
103impl<'a, DeviceClass, I: Ip, D: InterfaceProperties<DeviceClass>> Matcher<PacketOrigin<I, &'a D>>
104    for TrafficOriginMatcher<DeviceClass>
105{
106    fn matches(&self, actual: &PacketOrigin<I, &'a D>) -> bool {
107        match (self, actual) {
108            (
109                TrafficOriginMatcher::Local { bound_device_matcher },
110                PacketOrigin::Local { bound_address: _, bound_device },
111            ) => bound_device_matcher.matches(bound_device),
112            (
113                TrafficOriginMatcher::NonLocal,
114                PacketOrigin::NonLocal { source_address: _, incoming_device: _ },
115            ) => true,
116            (TrafficOriginMatcher::Local { .. }, PacketOrigin::NonLocal { .. })
117            | (TrafficOriginMatcher::NonLocal, PacketOrigin::Local { .. }) => false,
118        }
119    }
120}
121
122/// Contains traffic matchers for a given rule.
123///
124/// `None` fields match all packets.
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct RuleMatcher<I: Ip, DeviceClass> {
127    /// Matches on [`PacketOrigin`]'s bound address for a locally generated packet or the source
128    /// address of an incoming packet.
129    ///
130    /// Matches whether the source address of the packet is from the subnet. If the matcher is
131    /// specified but the source address is not specified, it resolves to not a match.
132    pub source_address_matcher: Option<SubnetMatcher<I::Addr>>,
133    /// Matches on [`PacketOrigin`]'s bound device for a locally generated packets or the receiving
134    /// device of an incoming packet.
135    pub traffic_origin_matcher: Option<TrafficOriginMatcher<DeviceClass>>,
136    /// Matches on [`RuleInput`]'s marks.
137    pub mark_matchers: MarkMatchers,
138}
139
140impl<I: Ip, DeviceClass> RuleMatcher<I, DeviceClass> {
141    /// Creates a rule matcher that matches all packets.
142    pub fn match_all_packets() -> Self {
143        RuleMatcher {
144            source_address_matcher: None,
145            traffic_origin_matcher: None,
146            mark_matchers: MarkMatchers::default(),
147        }
148    }
149}
150
151/// Packet properties used as input for the rules engine.
152pub struct RuleInput<'a, I: Ip, D> {
153    pub(crate) packet_origin: PacketOrigin<I, &'a D>,
154    pub(crate) marks: &'a Marks,
155}
156
157impl<'a, I: Ip, D: InterfaceProperties<DeviceClass>, DeviceClass> Matcher<RuleInput<'a, I, D>>
158    for RuleMatcher<I, DeviceClass>
159{
160    fn matches(&self, actual: &RuleInput<'a, I, D>) -> bool {
161        let Self { source_address_matcher, traffic_origin_matcher, mark_matchers } = self;
162        let RuleInput { packet_origin, marks } = actual;
163        source_address_matcher.matches(packet_origin)
164            && traffic_origin_matcher.matches(packet_origin)
165            && mark_matchers.matches(marks)
166    }
167}
168
169#[cfg(test)]
170mod test {
171    use ip_test_macro::ip_test;
172    use net_types::SpecifiedAddr;
173    use net_types::ip::Subnet;
174    use netstack3_base::InterfaceMatcher;
175    use netstack3_base::testutil::{FakeDeviceId, MultipleDevicesId, TestIpExt};
176    use test_case::test_case;
177
178    use super::*;
179
180    #[ip_test(I)]
181    #[test_case(None, None => true)]
182    #[test_case(None, Some(MultipleDevicesId::A) => true)]
183    #[test_case(
184        Some(BoundInterfaceMatcher::Unbound),
185        None => true)]
186    #[test_case(
187        Some(BoundInterfaceMatcher::Unbound),
188        Some(MultipleDevicesId::A) => false)]
189    #[test_case(
190        Some(BoundInterfaceMatcher::Bound(InterfaceMatcher::Name("A".into()))),
191        None => false)]
192    #[test_case(
193        Some(BoundInterfaceMatcher::Bound(InterfaceMatcher::Name("A".into()))),
194        Some(MultipleDevicesId::A) => true)]
195    #[test_case(
196        Some(BoundInterfaceMatcher::Bound(InterfaceMatcher::Name("A".into()))),
197        Some(MultipleDevicesId::B) => false)]
198    fn rule_matcher_matches_bound_device<I: TestIpExt>(
199        bound_device_matcher: Option<BoundInterfaceMatcher<()>>,
200        bound_device: Option<MultipleDevicesId>,
201    ) -> bool {
202        let matcher = RuleMatcher::<I, ()> {
203            traffic_origin_matcher: Some(TrafficOriginMatcher::Local { bound_device_matcher }),
204            ..RuleMatcher::match_all_packets()
205        };
206        let input = RuleInput {
207            packet_origin: PacketOrigin::Local {
208                bound_address: None,
209                bound_device: bound_device.as_ref(),
210            },
211            marks: &Default::default(),
212        };
213        matcher.matches(&input)
214    }
215
216    #[ip_test(I)]
217    #[test_case(None, None => true)]
218    #[test_case(None, Some(I::LOOPBACK_ADDRESS) => true)]
219    #[test_case(
220        Some(<I as TestIpExt>::TEST_ADDRS.subnet),
221        None => false)]
222    #[test_case(
223        Some(<I as TestIpExt>::TEST_ADDRS.subnet),
224        Some(<I as TestIpExt>::TEST_ADDRS.local_ip) => true)]
225    #[test_case(
226        Some(<I as TestIpExt>::TEST_ADDRS.subnet),
227        Some(<I as TestIpExt>::get_other_remote_ip_address(1)) => false)]
228    fn rule_matcher_matches_local_addr<I: TestIpExt>(
229        source_address_subnet: Option<Subnet<I::Addr>>,
230        bound_address: Option<SpecifiedAddr<I::Addr>>,
231    ) -> bool {
232        let matcher = RuleMatcher::<I, ()> {
233            source_address_matcher: source_address_subnet.map(SubnetMatcher),
234            ..RuleMatcher::match_all_packets()
235        };
236        let marks = Default::default();
237        let input = RuleInput::<'_, _, FakeDeviceId> {
238            packet_origin: PacketOrigin::Local { bound_address, bound_device: None },
239            marks: &marks,
240        };
241        matcher.matches(&input)
242    }
243
244    #[ip_test(I)]
245    #[test_case(None, PacketOrigin::Local {
246         bound_address: None,
247         bound_device: None
248    } => true)]
249    #[test_case(None, PacketOrigin::NonLocal {
250        source_address: <I as TestIpExt>::TEST_ADDRS.remote_ip,
251        incoming_device: &FakeDeviceId
252    } => true)]
253    #[test_case(Some(TrafficOriginMatcher::Local {
254        bound_device_matcher: None
255    }), PacketOrigin::Local {
256        bound_address: None,
257        bound_device: None
258    } => true)]
259    #[test_case(Some(TrafficOriginMatcher::NonLocal),
260        PacketOrigin::NonLocal {
261            source_address: <I as TestIpExt>::TEST_ADDRS.remote_ip,
262            incoming_device: &FakeDeviceId
263        } => true)]
264    #[test_case(Some(TrafficOriginMatcher::Local { bound_device_matcher: None }),
265        PacketOrigin::NonLocal {
266            source_address: <I as TestIpExt>::TEST_ADDRS.remote_ip,
267            incoming_device: &FakeDeviceId
268        }  => false)]
269    #[test_case(Some(TrafficOriginMatcher::NonLocal),
270        PacketOrigin::Local {
271            bound_address: None,
272            bound_device: None
273        } => false)]
274    fn rule_matcher_matches_locally_generated<I: TestIpExt>(
275        traffic_origin_matcher: Option<TrafficOriginMatcher<()>>,
276        packet_origin: PacketOrigin<I, &'static FakeDeviceId>,
277    ) -> bool {
278        let matcher =
279            RuleMatcher::<I, ()> { traffic_origin_matcher, ..RuleMatcher::match_all_packets() };
280        let marks = Default::default();
281        let input = RuleInput::<'_, _, FakeDeviceId> { packet_origin, marks: &marks };
282        matcher.matches(&input)
283    }
284
285    #[ip_test(I)]
286    #[test_case::test_matrix(
287            [
288                None,
289                Some(<I as TestIpExt>::TEST_ADDRS.local_ip),
290                Some(<I as TestIpExt>::get_other_remote_ip_address(1))
291            ],
292            [
293                None,
294                Some(&MultipleDevicesId::A),
295                Some(&MultipleDevicesId::B),
296                Some(&MultipleDevicesId::C),
297            ],
298            [true, false]
299        )]
300    fn rule_matcher_matches_multiple_conditions<I: TestIpExt>(
301        ip: Option<SpecifiedAddr<I::Addr>>,
302        device: Option<&'static MultipleDevicesId>,
303        locally_generated: bool,
304    ) {
305        let matcher = RuleMatcher::<I, ()> {
306            source_address_matcher: Some(SubnetMatcher(I::TEST_ADDRS.subnet)),
307            traffic_origin_matcher: Some(TrafficOriginMatcher::Local {
308                bound_device_matcher: Some(BoundInterfaceMatcher::Bound(InterfaceMatcher::Name(
309                    "A".into(),
310                ))),
311            }),
312            ..RuleMatcher::match_all_packets()
313        };
314
315        let packet_origin = if locally_generated {
316            PacketOrigin::Local { bound_address: ip, bound_device: device }
317        } else {
318            let (Some(source_address), Some(incoming_device)) = (ip, device) else {
319                return;
320            };
321            PacketOrigin::NonLocal { source_address, incoming_device }
322        };
323
324        let input = RuleInput { packet_origin, marks: &Default::default() };
325
326        if ip == Some(I::TEST_ADDRS.local_ip)
327            && (device == Some(&MultipleDevicesId::A))
328            && locally_generated
329        {
330            assert!(matcher.matches(&input))
331        } else {
332            assert!(!matcher.matches(&input))
333        }
334    }
335}