reachability_core/
route_table.rs

1// Copyright 2023 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 fidl_fuchsia_net_ext::IntoExt;
6use log::error;
7use net_types::ip::{GenericOverIp, Ip, IpAddress, Ipv4, Ipv6};
8use std::collections::HashSet;
9use {fidl_fuchsia_net as fnet, fidl_fuchsia_net_routes_ext as fnet_routes_ext};
10
11/// Keeps track of the current routing state, as observed from the Netstack
12/// `fuchsia.net.routes/WatcherV{4,6}` protocols.
13#[derive(Debug, Default)]
14pub struct RouteTable {
15    v4_routes: HashSet<fnet_routes_ext::InstalledRoute<Ipv4>>,
16    v6_routes: HashSet<fnet_routes_ext::InstalledRoute<Ipv6>>,
17}
18
19/// A flattened version of [`fnet_routes_ext::InstalledRoute`] that drops fields
20/// irrelevant for Reachability and converts address types to `fnet`.
21#[derive(Debug, PartialEq)]
22pub(crate) struct Route {
23    pub(crate) destination: fnet::Subnet,
24    pub(crate) outbound_interface: u64,
25    pub(crate) next_hop: Option<fnet::IpAddress>,
26}
27
28pub(crate) struct RouteActionNotForwardingError {}
29
30// Implement conversions from `InstalledRoute` to `Route` which is
31// fallible iff, the route's action is not `Forward`.
32impl<I: Ip> TryFrom<fnet_routes_ext::InstalledRoute<I>> for Route {
33    type Error = RouteActionNotForwardingError;
34    fn try_from(
35        fnet_routes_ext::InstalledRoute {
36            route: fnet_routes_ext::Route { destination, action, properties: _ },
37            effective_properties: _,
38            // TODO(https://fxbug.dev/342634598): Reachability monitor should
39            // use the route table for the network it is monitoring.
40            table_id: _,
41        }: fnet_routes_ext::InstalledRoute<I>,
42    ) -> Result<Self, Self::Error> {
43        match action {
44            fnet_routes_ext::RouteAction::Forward(fnet_routes_ext::RouteTarget {
45                outbound_interface,
46                next_hop,
47            }) => Ok(Route {
48                destination: destination.into_ext(),
49                outbound_interface,
50                next_hop: next_hop.map(|next_hop| (*next_hop).to_ip_addr().into_ext()),
51            }),
52            fnet_routes_ext::RouteAction::Unknown => Err(RouteActionNotForwardingError {}),
53        }
54    }
55}
56
57// Implement optional conversions from `InstalledRoute` to `Route`.
58// `Ok` becomes `Some`, while `Err` is logged and becomes `None`.
59impl Route {
60    fn optionally_from<I: Ip>(route: fnet_routes_ext::InstalledRoute<I>) -> Option<Route> {
61        match route.try_into() {
62            Ok(route) => Some(route),
63            Err(RouteActionNotForwardingError {}) => {
64                error!("Unexpected route in routing table: {:?}", route);
65                None
66            }
67        }
68    }
69}
70
71impl RouteTable {
72    /// Constructs a new RouteTable with the provided existing routes.
73    pub fn new_with_existing_routes(
74        v4_routes: HashSet<fnet_routes_ext::InstalledRoute<Ipv4>>,
75        v6_routes: HashSet<fnet_routes_ext::InstalledRoute<Ipv6>>,
76    ) -> RouteTable {
77        return RouteTable { v4_routes, v6_routes };
78    }
79
80    // Returns a mutable reference to the underlying route table for the
81    // specified IP Protocol.
82    fn get_ip_specific_table_mut<I: Ip>(
83        &mut self,
84    ) -> &mut HashSet<fnet_routes_ext::InstalledRoute<I>> {
85        #[derive(GenericOverIp)]
86        #[generic_over_ip(I, Ip)]
87        struct TableHolder<'a, I: Ip>(&'a mut HashSet<fnet_routes_ext::InstalledRoute<I>>);
88        let TableHolder(ip_specific_table) = I::map_ip_out(
89            self,
90            |route_table| TableHolder(&mut route_table.v4_routes),
91            |route_table| TableHolder(&mut route_table.v6_routes),
92        );
93        ip_specific_table
94    }
95
96    // Adds the given route to this `RouteTable`; returns false if the route
97    // was already present, true otherwise.
98    pub fn add_route<I: Ip>(&mut self, route: fnet_routes_ext::InstalledRoute<I>) -> bool {
99        self.get_ip_specific_table_mut::<I>().insert(route)
100    }
101
102    // Removes the given route from this `RouteTable`; returns false if the
103    // route was not present, true otherwise.
104    pub fn remove_route<I: Ip>(&mut self, route: &fnet_routes_ext::InstalledRoute<I>) -> bool {
105        self.get_ip_specific_table_mut::<I>().remove(route)
106    }
107
108    /// Returns an Iterator of all routes via the given interface.
109    pub(crate) fn device_routes(&self, device: u64) -> impl Iterator<Item = Route> + '_ {
110        let RouteTable { v4_routes, v6_routes } = self;
111        v4_routes
112            .iter()
113            .filter_map(|route| Route::optionally_from(*route))
114            .chain(v6_routes.iter().filter_map(|route| Route::optionally_from(*route)))
115            .filter(move |Route { destination: _, outbound_interface, next_hop: _ }| {
116                *outbound_interface == device
117            })
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::testutil;
125    use net_declare::fidl_subnet;
126    use test_case::test_case;
127
128    #[test_case([], 1, []; "empty route table")]
129    #[test_case(
130        [Route{
131            destination: fidl_subnet!("192.0.2.0/24"),
132            outbound_interface: 2,
133            next_hop: None
134        },
135        Route{
136            destination: fidl_subnet!("2001:db8::/32"),
137            outbound_interface: 2,
138            next_hop: None
139        }],
140        1,
141        [];
142        "0 device routes")]
143    #[test_case(
144        [Route{
145            destination: fidl_subnet!("192.0.2.0/24"),
146            outbound_interface: 1,
147            next_hop: None
148        },
149        Route{
150            destination: fidl_subnet!("2001:db8::/32"),
151            outbound_interface: 2,
152            next_hop: None
153        }],
154        1,
155        [Route{
156            destination: fidl_subnet!("192.0.2.0/24"),
157            outbound_interface: 1,
158            next_hop: None
159        }];
160        "IPv4 device route")]
161    #[test_case(
162        [Route{
163            destination: fidl_subnet!("192.0.2.0/24"),
164            outbound_interface: 2,
165            next_hop: None
166        },
167        Route{
168            destination: fidl_subnet!("2001:db8::/32"),
169            outbound_interface: 1,
170            next_hop: None
171        }],
172        1,
173        [Route{
174            destination: fidl_subnet!("2001:db8::/32"),
175            outbound_interface: 1,
176            next_hop: None
177        }];
178        "IPv6 device route")]
179    #[test_case(
180        [Route{
181            destination: fidl_subnet!("192.0.2.0/24"),
182            outbound_interface: 1,
183            next_hop: None
184        },
185        Route{
186            destination: fidl_subnet!("2001:db8::/32"),
187            outbound_interface: 1,
188            next_hop: None
189        }],
190        1,
191        [Route{
192            destination: fidl_subnet!("192.0.2.0/24"),
193            outbound_interface: 1,
194            next_hop: None
195        },
196        Route{
197            destination: fidl_subnet!("2001:db8::/32"),
198            outbound_interface: 1,
199            next_hop: None
200        }];
201        "IPv4 & IPv6 device routes")]
202    fn device_routes(
203        all_routes: impl IntoIterator<Item = Route>,
204        device_id: u64,
205        expected_routes: impl IntoIterator<Item = Route>,
206    ) {
207        assert_eq!(
208            testutil::build_route_table_from_flattened_routes(all_routes)
209                .device_routes(device_id)
210                .collect::<Vec<_>>(),
211            expected_routes.into_iter().collect::<Vec<_>>()
212        );
213    }
214}