use std::io;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use ipnet::{IpNet, Ipv4Net, Ipv6Net};
use smallvec_wrapper::SmallVec;
use smol_str::SmolStr;
use super::os;
macro_rules! routev_impl {
($kind:literal) => {
paste::paste! {
#[doc = "An IP" $kind " entry from the kernel routing table."]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct [<Ip $kind Route>] {
index: u32,
destination: [<Ip $kind Net>],
gateway: Option<[<Ip $kind Addr>]>,
}
impl core::fmt::Display for [<Ip $kind Route>] {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self.gateway {
Some(gw) => write!(f, "{} via {} ({})", self.destination, gw, self.index),
None => write!(f, "{} ({})", self.destination, self.index),
}
}
}
impl [<Ip $kind Route>] {
#[doc = "Creates a new IP" $kind " route entry."]
#[inline]
pub const fn new(
index: u32,
destination: [<Ip $kind Net>],
gateway: Option<[<Ip $kind Addr>]>,
) -> Self {
Self { index, destination, gateway }
}
#[inline]
pub const fn index(&self) -> u32 {
self.index
}
pub fn name(&self) -> io::Result<SmolStr> {
crate::idx_to_name::ifindex_to_name(self.index)
}
#[inline]
pub const fn destination(&self) -> &[<Ip $kind Net>] {
&self.destination
}
#[inline]
pub const fn gateway(&self) -> Option<[<Ip $kind Addr>]> {
self.gateway
}
#[inline]
pub const fn is_default(&self) -> bool {
self.destination.prefix_len() == 0
}
}
}
};
}
routev_impl!("v4");
routev_impl!("v6");
impl From<Ipv4Route> for IpRoute {
#[inline]
fn from(value: Ipv4Route) -> Self {
Self::V4(value)
}
}
impl From<Ipv6Route> for IpRoute {
#[inline]
fn from(value: Ipv6Route) -> Self {
Self::V6(value)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum IpRoute {
V4(Ipv4Route),
V6(Ipv6Route),
}
impl core::fmt::Display for IpRoute {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::V4(r) => write!(f, "{r}"),
Self::V6(r) => write!(f, "{r}"),
}
}
}
impl IpRoute {
#[inline]
pub const fn index(&self) -> u32 {
match self {
Self::V4(r) => r.index(),
Self::V6(r) => r.index(),
}
}
pub fn name(&self) -> io::Result<SmolStr> {
crate::idx_to_name::ifindex_to_name(self.index())
}
#[inline]
pub const fn destination(&self) -> IpNet {
match self {
Self::V4(r) => IpNet::V4(*r.destination()),
Self::V6(r) => IpNet::V6(*r.destination()),
}
}
#[inline]
pub const fn gateway(&self) -> Option<IpAddr> {
match self {
Self::V4(r) => match r.gateway() {
Some(ip) => Some(IpAddr::V4(ip)),
None => None,
},
Self::V6(r) => match r.gateway() {
Some(ip) => Some(IpAddr::V6(ip)),
None => None,
},
}
}
#[inline]
pub const fn is_default(&self) -> bool {
match self {
Self::V4(r) => r.is_default(),
Self::V6(r) => r.is_default(),
}
}
}
pub fn route_table() -> io::Result<SmallVec<IpRoute>> {
os::route_table_by_filter(|_| true)
}
pub fn route_ipv4_table() -> io::Result<SmallVec<Ipv4Route>> {
os::route_ipv4_table_by_filter(|_| true)
}
pub fn route_ipv6_table() -> io::Result<SmallVec<Ipv6Route>> {
os::route_ipv6_table_by_filter(|_| true)
}
pub fn route_table_by_filter<F>(f: F) -> io::Result<SmallVec<IpRoute>>
where
F: FnMut(&IpRoute) -> bool,
{
os::route_table_by_filter(f)
}
pub fn route_ipv4_table_by_filter<F>(f: F) -> io::Result<SmallVec<Ipv4Route>>
where
F: FnMut(&Ipv4Route) -> bool,
{
os::route_ipv4_table_by_filter(f)
}
pub fn route_ipv6_table_by_filter<F>(f: F) -> io::Result<SmallVec<Ipv6Route>>
where
F: FnMut(&Ipv6Route) -> bool,
{
os::route_ipv6_table_by_filter(f)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn route_v4_basic() {
let dst = Ipv4Net::new(Ipv4Addr::new(10, 0, 0, 0), 8).unwrap();
let gw = Some(Ipv4Addr::new(10, 0, 0, 1));
let r = Ipv4Route::new(2, dst, gw);
assert_eq!(r.index(), 2);
assert_eq!(r.destination(), &dst);
assert_eq!(r.gateway(), gw);
assert!(!r.is_default());
let default = Ipv4Route::new(0, Ipv4Net::new(Ipv4Addr::UNSPECIFIED, 0).unwrap(), None);
assert!(default.is_default());
assert!(default.gateway().is_none());
}
#[test]
fn route_v6_basic() {
let dst = Ipv6Net::new(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0), 32).unwrap();
let gw = Some(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1));
let r = Ipv6Route::new(3, dst, gw);
assert_eq!(r.index(), 3);
assert_eq!(r.destination(), &dst);
assert_eq!(r.gateway(), gw);
assert!(!r.is_default());
}
#[test]
fn route_enum_dispatch() {
let v4 = Ipv4Route::new(
1,
Ipv4Net::new(Ipv4Addr::new(192, 168, 1, 0), 24).unwrap(),
None,
);
let r: IpRoute = v4.into();
assert_eq!(r.index(), 1);
assert!(r.gateway().is_none());
assert!(matches!(r.destination(), IpNet::V4(_)));
assert!(!r.is_default());
let v6 = Ipv6Route::new(
1,
Ipv6Net::new(Ipv6Addr::UNSPECIFIED, 0).unwrap(),
Some(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1)),
);
let r: IpRoute = v6.into();
assert!(r.is_default());
assert!(matches!(r.destination(), IpNet::V6(_)));
assert_eq!(
r.gateway(),
Some(IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1))),
);
}
#[cfg(not(target_os = "netbsd"))]
#[test]
fn route_table_returns() {
let routes = route_table().unwrap();
for r in &routes {
let _ = r.index();
let _ = r.destination();
let _ = r.gateway();
let _ = r.is_default();
let _ = format!("{r}");
}
}
#[cfg(not(target_os = "netbsd"))]
#[test]
fn route_table_filter_default_only() {
let defaults = route_table_by_filter(|r| r.is_default()).unwrap();
for r in &defaults {
assert!(
r.is_default(),
"got non-default route through is_default filter: {r}"
);
}
}
#[test]
fn route_v4_table_returns() {
let routes = route_ipv4_table().unwrap();
for r in &routes {
let _ = r.index();
let _ = r.destination();
let _ = r.gateway();
}
}
#[cfg(not(target_os = "netbsd"))]
#[test]
fn route_v6_table_returns() {
let routes = route_ipv6_table().unwrap();
for r in &routes {
let _ = r.index();
let _ = r.destination();
let _ = r.gateway();
}
}
}