net_route/
lib.rs

1// SPDX-License-Identifier: MIT
2
3//! This project aims to provide a high level interface for manipulating and observing
4//! the routing table on a variety of platforms.
5//!
6//!
7//! ## Examples
8//! #### Adding a route
9//! ```no_run
10//! // route traffic destined for 10.14.0.0/24 to 192.1.2.1 using interface 9
11//! # use net_route::{Handle, Route};
12//! # #[tokio::main]
13//! # async fn main() -> std::io::Result<()> {
14//! let handle = Handle::new()?;
15//! let route = Route::new("10.14.0.0".parse().unwrap(), 24)
16//!     .with_ifindex(9)
17//!     .with_gateway("192.1.2.1".parse().unwrap());
18//! handle.add(&route).await
19//! # }
20//! ```
21//!
22//! #### Listening to changes in the routing table
23//! ```no_run
24//! # use futures::StreamExt;
25//! # use net_route::Handle;
26//! # #[tokio::main]
27//! # async fn main() -> std::io::Result<()> {
28//! let handle = Handle::new()?;
29//! let stream = handle.route_listen_stream();
30//! futures::pin_mut!(stream);
31//! while let Some(event) = stream.next().await {
32//!     println!("{:?}", event);
33//! }
34//! # Ok(())
35//! # }
36//! ```
37
38use std::{
39    io,
40    net::{IpAddr, Ipv4Addr, Ipv6Addr},
41};
42
43mod platform_impl;
44use platform_impl::PlatformHandle;
45
46#[cfg(all(target_os = "macos", not(doc)))]
47pub use platform_impl::ifname_to_index;
48
49/// Handle that abstracts initialization and cleanup of resources needed to operate on the routing table.
50pub struct Handle(PlatformHandle);
51
52impl Handle {
53    pub fn new() -> io::Result<Self> {
54        Ok(Self(PlatformHandle::new()?))
55    }
56
57    /// Add route to the system's routing table.
58    pub async fn add(&self, route: &Route) -> io::Result<()> {
59        self.0.add(route).await
60    }
61
62    /// Returns a `Stream` which will yield a `RouteChange` event whenever a route is added, removed, or changed from the system's routing table.
63    pub fn route_listen_stream(&self) -> impl futures::Stream<Item = RouteChange> {
64        self.0.route_listen_stream()
65    }
66
67    /// Returns a `Vec<Route>` containing a list of both ipv4 and v6 routes on the system.
68    pub async fn list(&self) -> io::Result<Vec<Route>> {
69        self.0.list().await
70    }
71
72    /// Get one of the default routes on the system if there is at least one.
73    pub async fn default_route(&self) -> io::Result<Option<Route>> {
74        self.0.default_route().await
75    }
76
77    /// Remove a route from the system's routing table.
78    pub async fn delete(&self, route: &Route) -> io::Result<()> {
79        self.0.delete(route).await
80    }
81}
82
83/// Contains information that describes a route in the local computer's Ipv4 or Ipv6 routing table.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct Route {
86    /// Network address of the destination. `0.0.0.0` with a prefix of `0` is considered a default route.
87    pub destination: IpAddr,
88
89    /// Length of network prefix in the destination address.
90    pub prefix: u8,
91
92    /// The address of the next hop of this route.
93    ///
94    /// On macOS, this must be `Some` if ifindex is `None`
95    pub gateway: Option<IpAddr>,
96
97    /// The index of the local interface through which the next hop of this route may be reached.
98    ///
99    /// On macOS, this must be `Some` if gateway is `None`
100    pub ifindex: Option<u32>,
101
102    #[cfg(target_os = "linux")]
103    /// The routing table this route belongs to.
104    pub table: u8,
105
106    /// Network address of the source.
107    #[cfg(target_os = "linux")]
108    pub source: Option<IpAddr>,
109
110    /// Prefix length of the source address.
111    #[cfg(target_os = "linux")]
112    pub source_prefix: u8,
113
114    /// Source address hint. Does not influence routing.
115    #[cfg(target_os = "linux")]
116    pub source_hint: Option<IpAddr>,
117
118    #[cfg(any(target_os = "windows", target_os = "linux"))]
119    /// The route metric offset value for this route.
120    pub metric: Option<u32>,
121
122    #[cfg(target_os = "windows")]
123    /// Luid of the local interface through which the next hop of this route may be reached.
124    ///
125    /// If luid is specified, ifindex is optional.
126    pub luid: Option<u64>,
127}
128
129impl Route {
130    /// Create a route that matches a given destination network.
131    ///
132    /// Either the gateway or interface should be set before attempting to add to a routing table.
133    pub fn new(destination: IpAddr, prefix: u8) -> Self {
134        Self {
135            destination,
136            prefix,
137            gateway: None,
138            ifindex: None,
139            #[cfg(target_os = "linux")]
140            // default to main table
141            table: 254,
142            #[cfg(target_os = "linux")]
143            source: None,
144            #[cfg(target_os = "linux")]
145            source_prefix: 0,
146            #[cfg(target_os = "linux")]
147            source_hint: None,
148            #[cfg(any(target_os = "windows", target_os = "linux"))]
149            metric: None,
150            #[cfg(target_os = "windows")]
151            luid: None,
152        }
153    }
154
155    /// Set the next next hop gateway for this route.
156    pub fn with_gateway(mut self, gateway: IpAddr) -> Self {
157        self.gateway = Some(gateway);
158        self
159    }
160
161    /// Set the index of the local interface through which the next hop of this route should be reached.
162    pub fn with_ifindex(mut self, ifindex: u32) -> Self {
163        self.ifindex = Some(ifindex);
164        self
165    }
166
167    /// Set table the route will be installed in.
168    #[cfg(target_os = "linux")]
169    pub fn with_table(mut self, table: u8) -> Self {
170        self.table = table;
171        self
172    }
173
174    /// Set source.
175    #[cfg(target_os = "linux")]
176    pub fn with_source(mut self, source: IpAddr, prefix: u8) -> Self {
177        self.source = Some(source);
178        self.source_prefix = prefix;
179        self
180    }
181
182    /// Set source hint.
183    #[cfg(target_os = "linux")]
184    pub fn with_source_hint(mut self, hint: IpAddr) -> Self {
185        self.source_hint = Some(hint);
186        self
187    }
188
189    /// Set route metric.
190    #[cfg(any(target_os = "windows", target_os = "linux"))]
191    pub fn with_metric(mut self, metric: u32) -> Self {
192        self.metric = Some(metric);
193        self
194    }
195
196    /// Set luid of the local interface through which the next hop of this route should be reached.
197    #[cfg(target_os = "windows")]
198    pub fn with_luid(mut self, luid: u64) -> Self {
199        self.luid = Some(luid);
200        self
201    }
202
203    /// Get the netmask covering the network portion of the destination address.
204    pub fn mask(&self) -> IpAddr {
205        match self.destination {
206            IpAddr::V4(_) => IpAddr::V4(Ipv4Addr::from(
207                u32::MAX.checked_shl(32 - self.prefix as u32).unwrap_or(0),
208            )),
209            IpAddr::V6(_) => IpAddr::V6(Ipv6Addr::from(
210                u128::MAX.checked_shl(128 - self.prefix as u32).unwrap_or(0),
211            )),
212        }
213    }
214}
215
216#[derive(Debug, Clone, PartialEq, Eq)]
217pub enum RouteChange {
218    Add(Route),
219    Delete(Route),
220    Change(Route),
221}
222
223#[cfg(test)]
224mod tests {
225    use std::net::{IpAddr, Ipv6Addr};
226
227    use crate::Route;
228
229    #[test]
230    fn it_calculates_v4_netmask() {
231        let mut route = Route::new("10.10.0.0".parse().unwrap(), 32);
232
233        assert_eq!(route.mask(), "255.255.255.255".parse::<IpAddr>().unwrap());
234
235        route.prefix = 29;
236        assert_eq!(route.mask(), "255.255.255.248".parse::<IpAddr>().unwrap());
237
238        route.prefix = 25;
239        assert_eq!(route.mask(), "255.255.255.128".parse::<IpAddr>().unwrap());
240
241        route.prefix = 2;
242        assert_eq!(route.mask(), "192.0.0.0".parse::<IpAddr>().unwrap());
243    }
244
245    #[test]
246    fn it_calculates_v6_netmask() {
247        let route = Route::new(
248            "77ca:838b:9ec0:fc97:eedc:236a:9d41:31e5".parse().unwrap(),
249            32,
250        );
251        assert_eq!(
252            route.mask(),
253            Ipv6Addr::new(0xffff, 0xffff, 0, 0, 0, 0, 0, 0)
254        );
255    }
256}