//! What should be in the kernel's table, what is, and what to do about the //! difference. No I/O here: this is told what the routing table's best //! routes are and what the kernel said, and answers with the changes to //! ask the kernel for. use std::net::{IpAddr, Ipv6Addr}; use bgp_packet::message::NlriNextHop; use bgp_primitives::constants::IpVersion; use bgp_primitives::ip_prefix::IpPrefix; use crate::netlink::KIND_UNICAST; use crate::topology::Topology; // Asked for every route: a quick hash matters here. pub(crate) type HashMap = std::collections::HashMap; pub(crate) type HashSet = std::collections::HashSet; /// Where the kernel is to send packets for a prefix. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Target { /// To a gateway, out of an interface. Via { gateway: IpAddr, /// The index of the interface the gateway is on. interface: u32, }, /// Nowhere: the packets are dropped. For a route that asks for that /// with the BLACKHOLE community (RFC 7999). Discard, } /// Why a route's next hop is not something the kernel can be given. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Unresolved { /// The route has no next hop. Missing, /// An IPv6 route with an IPv4 next hop. OtherFamily, /// The next hop is one of this machine's own addresses. Ourselves, /// The next hop is not in the network of any interface that is up, /// and none of the kernel's routes that count says the way to it /// (see `crate::routes`). NoRoute, /// The only route to the next hop is the default route, which is not /// taken as the way to a next hop unless asked for /// (`Topology::via_default`). OnlyDefault, /// The route to the next hop is one that discards packets: a /// blackhole, unreachable or prohibit route. Discarded, /// The route is for a network this machine is on itself, or for part /// of one. The kernel reaches that directly; a route from a peer that /// said otherwise would send the neighbors' traffic, and the sessions /// with them, wherever the peer liked. OwnNetwork, } impl Unresolved { pub const ALL: [Self; 7] = [ Self::Missing, Self::OtherFamily, Self::Ourselves, Self::NoRoute, Self::OnlyDefault, Self::Discarded, Self::OwnNetwork, ]; pub fn as_str(self) -> &'static str { match self { Self::Missing => "no next hop", Self::OtherFamily => "IPv4 next hop for an IPv6 route", Self::Ourselves => "next hop is this machine", Self::NoRoute => "no route to the next hop", Self::OnlyDefault => "next hop reachable only by the default route", Self::Discarded => "the route to the next hop discards packets", Self::OwnNetwork => "inside a network of this machine", } } } // A reason's place in `ALL` is its number as declared, which is what // [`Plan`] counts them by. const _: () = { let mut at = 0; while at < Unresolved::ALL.len() { assert!(Unresolved::ALL[at] as usize == at); at += 1; } }; /// The peer a route was learned from, as far as its next hop is concerned. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Neighbor { pub address: IpAddr, /// The interface the session with the peer is on, where the peer's /// address is a link-local one and says nothing without it; zero /// otherwise. pub interface: u32, } impl From for Neighbor { fn from(address: IpAddr) -> Self { Self { address, interface: 0, } } } /// Works out where the kernel is to send packets for a route to `prefix` /// with the next hop `next_hop`, learned from `peer`. `blackhole` says /// the route asks for its packets to be dropped (the BLACKHOLE community, /// RFC 7999): then they go nowhere, and the next hop is not looked at. /// /// A next hop that is a neighbor on one of our interfaces is where the /// packets go. One that is not is looked up in the kernel's own routes, /// and the packets go where the route that covers it sends them: to its /// gateway, or onto its interface. A link-local next hop means something /// only on one link, and that is taken to be the link the peer itself is /// on. pub fn resolve( topology: &Topology, prefix: &IpPrefix, next_hop: Option<&NlriNextHop>, peer: Option, blackhole: bool, ) -> Result { if topology.interfaces.has_network_around(prefix) { return Err(Unresolved::OwnNetwork); } if blackhole { return Ok(Target::Discard); } resolve_next_hop(topology, prefix.address_family(), next_hop, peer) } /// The way to `gateway`, which is not a neighbor, by the kernel's routes /// that count: where the route that covers it sends packets. One step /// only: the kernel sees to it that a route's own gateway is a neighbor. fn through_routes( topology: &Topology, routes: IpVersion, gateway: IpAddr, ) -> Result { let route = topology.routes.lookup(gateway).ok_or(Unresolved::NoRoute)?; if route.prefix.length() == 0 && !topology.via_default { return Err(Unresolved::OnlyDefault); } if route.kind != KIND_UNICAST { return Err(Unresolved::Discarded); } let (hop, interface) = match route.nexthop { Some(id) => { // A group of nexthops has no gateway or interface of its own; // which of its members packets take is the kernel's to say, // and nothing to give it as a route. let nexthop = topology.nexthops.get(&id).ok_or(Unresolved::NoRoute)?; ( nexthop.gateway, nexthop.interface.ok_or(Unresolved::NoRoute)?, ) } None => (route.gateway, route.interface.ok_or(Unresolved::NoRoute)?), }; if !topology .interfaces .link(interface) .is_some_and(|link| link.up) { return Err(Unresolved::NoRoute); } // A route without a gateway puts the destination on its interface: // the next hop itself is the neighbor there. let gateway = hop.unwrap_or(gateway); // An IPv4 route may go by an IPv6 gateway (RFC 8950, and the kernel // allows it); an IPv6 route by an IPv4 gateway is nothing. if routes == IpVersion::Ipv6 && gateway.is_ipv4() { return Err(Unresolved::NoRoute); } Ok(Target::Via { gateway, interface }) } /// The part of [`resolve`] that is the same for every route of IP version /// `routes` with one next hop from one peer: where the next hop is. For /// those who ask it of many such routes, and ask of each only whether it /// is for one of the machine's own networks. pub fn resolve_next_hop( topology: &Topology, routes: IpVersion, next_hop: Option<&NlriNextHop>, peer: Option, ) -> Result { let interfaces = &topology.interfaces; let next_hop = next_hop.ok_or(Unresolved::Missing)?; let version = match next_hop { NlriNextHop::Ipv4(_) => IpVersion::Ipv4, NlriNextHop::Ipv6(_) | NlriNextHop::Ipv6WithLl { .. } => IpVersion::Ipv6, }; // An IPv4 route may have an IPv6 next hop: that is how routes go over // a link with only IPv6 addresses (RFC 8950), and the kernel takes // such a route. The other way about means nothing. if version != routes && version == IpVersion::Ipv4 { return Err(Unresolved::OtherFamily); } let direct = |gateway: IpAddr| { if interfaces.is_local(gateway) { return Err(Unresolved::Ourselves); } match interfaces.connected(gateway) { Some(connected) => Ok(Target::Via { gateway, interface: connected.interface, }), None => through_routes(topology, routes, gateway), } }; // The interface the peer is a neighbor on, for a link-local gateway. let on_peers_link = |gateway: Ipv6Addr| { let interface = match peer.ok_or(Unresolved::NoRoute)? { // The session is on a link-local address itself, and so says // which link: every interface has fe80::/64, and looking the // peer up there would find any of them. Neighbor { interface, .. } if interface != 0 => interfaces .link(interface) .filter(|link| link.up) .map(|link| link.index), Neighbor { address, .. } => interfaces .connected(address) .map(|connected| connected.interface), } .ok_or(Unresolved::NoRoute)?; if interfaces .addresses_of(interface) .any(|ours| ours.local == IpAddr::V6(gateway)) { return Err(Unresolved::Ourselves); } Ok(Target::Via { gateway: gateway.into(), interface, }) }; match *next_hop { NlriNextHop::Ipv4(gateway) => direct(gateway.into()), NlriNextHop::Ipv6(gateway) if gateway.is_unicast_link_local() => on_peers_link(gateway), NlriNextHop::Ipv6(gateway) => direct(gateway.into()), // The global address if it is a neighbor; else the link-local one, // which is there for exactly that case (RFC 2545 ยง3). // Some put the link-local address, or nothing, where the global // one goes, having no global one to give. NlriNextHop::Ipv6WithLl { global, link_local } if global.is_unspecified() || global.is_unicast_link_local() => { on_peers_link(link_local) } // Where there is no way to the global one, the link-local one is // tried; a way that is there and is bad (ourselves, a blackhole) // is an answer as it stands. NlriNextHop::Ipv6WithLl { global, link_local } => match direct(global.into()) { Err(Unresolved::NoRoute | Unresolved::OnlyDefault) => on_peers_link(link_local), resolved => resolved, }, } } /// A change to ask the kernel for. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Op { /// Put this route there, in place of ours for the prefix if there is /// one. Install { prefix: IpPrefix, target: Target, }, Remove { prefix: IpPrefix, }, } impl Op { pub fn prefix(&self) -> &IpPrefix { match self { Self::Install { prefix, .. } | Self::Remove { prefix } => prefix, } } } struct Entry { /// What the kernel has; `None` for a route found there at startup that /// is not of a shape we make. target: Option, /// Whether the prefix was heard of since the last reset. confirmed: bool, /// Whether the route is one an earlier run left that is being kept /// for now though not confirmed: see [`Plan::adopt`]. kept: bool, } /// The routes of ours in the kernel, kept in step with the best routes. /// /// It follows what a reader of the routing table is given: the whole /// table between a reset and its completion, and changes after. Whatever /// is in the kernel and was not confirmed by the end of a whole table is /// removed: that is how routes left by an earlier run, or made stale by a /// change of interfaces, are cleared out. #[derive(Default)] pub struct Plan { /// What the kernel has been asked to have, and has not refused. installed: HashMap, /// Prefixes whose best route is not installed, and why. unresolved: HashMap, /// How many of them for each reason, in the order of /// [`Unresolved::ALL`]. unresolved_counts: [usize; Unresolved::ALL.len()], /// Prefixes whose route the kernel refused. refused: HashSet, /// How many entries of `installed` are blackhole routes, and how many /// are kept from an earlier run without having been confirmed. Kept in /// step with the map by [`Plan::put`] and [`Plan::take`], so that /// asking costs nothing however large the table: the kernel task asks /// after every batch of changes, and a scan of a million entries each /// time was a third of its work. blackholes: usize, kept_unconfirmed: usize, } impl Plan { /// How many routes of ours the kernel has. pub fn installed(&self) -> usize { self.installed.len() } /// How many of the routes the kernel has drop their packets. pub fn blackholes(&self) -> usize { self.blackholes } /// What `entry` counts for: one if it is a blackhole route, and one if /// it is kept from an earlier run without having been confirmed. fn weigh(entry: &Entry) -> (usize, usize) { ( usize::from(entry.target == Some(Target::Discard)), usize::from(entry.kept && !entry.confirmed), ) } /// Puts `entry` in as the route of `prefix`, in place of what was /// there, keeping the counts in step. fn put(&mut self, prefix: IpPrefix, entry: Entry) { let (blackhole, kept) = Self::weigh(&entry); self.blackholes += blackhole; self.kept_unconfirmed += kept; if let Some(old) = self.installed.insert(prefix, entry) { let (blackhole, kept) = Self::weigh(&old); self.blackholes -= blackhole; self.kept_unconfirmed -= kept; } } /// Takes the route of `prefix` out, keeping the counts in step. fn take(&mut self, prefix: &IpPrefix) -> Option { let old = self.installed.remove(prefix)?; let (blackhole, kept) = Self::weigh(&old); self.blackholes -= blackhole; self.kept_unconfirmed -= kept; Some(old) } /// How many prefixes have a best route that could not be given to the /// kernel for want of a usable next hop. pub fn unresolved(&self) -> usize { self.unresolved.len() } /// The same, by reason, leaving out the reasons there are none of. pub fn unresolved_by_reason(&self) -> impl Iterator { Unresolved::ALL .into_iter() .zip(self.unresolved_counts) .filter(|(_, count)| *count > 0) } /// Notes why the best route of `prefix` is not installed, or with /// `None` that this is no longer so. fn set_unresolved(&mut self, prefix: IpPrefix, reason: Option) { let before = match reason { Some(reason) => self.unresolved.insert(prefix, reason), None => self.unresolved.remove(&prefix), }; if let Some(before) = before { self.unresolved_counts[before as usize] -= 1; } if let Some(reason) = reason { self.unresolved_counts[reason as usize] += 1; } } /// Forgets which prefixes are unresolved or refused, and why. fn forget_unresolved_and_refused(&mut self) { self.unresolved.clear(); self.unresolved_counts = [0; Unresolved::ALL.len()]; self.refused.clear(); } /// How many prefixes have a best route that the kernel refused. pub fn refused(&self) -> usize { self.refused.len() } /// Notes a route of ours that the kernel already has. It stays only if /// the routing table turns out to have the same. /// /// With `keep` it is not removed for being absent from a whole table, /// only by [`Plan::release`]. That is for the routes an earlier run /// left: when the daemon starts, its table is empty until its peers /// have sent theirs again, and to remove everything meanwhile would /// stop the machine forwarding for as long as that takes. pub fn adopt(&mut self, prefix: IpPrefix, target: Option, keep: bool) { self.put( prefix, Entry { target, confirmed: false, kept: keep, }, ); } /// How many routes are being kept from an earlier run without the /// routing table having confirmed them. pub fn kept(&self) -> usize { self.kept_unconfirmed } /// The time for keeping unconfirmed routes of an earlier run is over: /// those the routing table still does not have go. pub fn release(&mut self) -> Vec { let mut stale = Vec::new(); let mut blackholes_gone = 0; self.installed.retain(|prefix, entry| { let goes = entry.kept && !entry.confirmed; entry.kept = false; if goes { stale.push(Op::Remove { prefix: *prefix }); blackholes_gone += usize::from(entry.target == Some(Target::Discard)); } !goes }); // What stays was confirmed, and is now not kept either. self.kept_unconfirmed = 0; self.blackholes -= blackholes_gone; stale } /// The whole table follows: nothing is known to be wanted until it is /// heard of again. pub fn reset(&mut self) { self.kept_unconfirmed = 0; for entry in self.installed.values_mut() { entry.confirmed = false; self.kept_unconfirmed += usize::from(entry.kept); } self.forget_unresolved_and_refused(); } /// The best route for `prefix` is now one that resolves as `target`. pub fn best(&mut self, prefix: IpPrefix, target: Result) -> Option { // Nearly always there is nothing to forget. if !self.refused.is_empty() { self.refused.remove(&prefix); } match target { Ok(target) => { if !self.unresolved.is_empty() { self.set_unresolved(prefix, None); } let entry = self.installed.entry(prefix).or_insert(Entry { target: None, confirmed: true, kept: false, }); let before = Self::weigh(entry); entry.confirmed = true; let op = if entry.target == Some(target) { None } else { entry.target = Some(target); Some(Op::Install { prefix, target }) }; let after = Self::weigh(entry); self.blackholes += after.0; self.blackholes -= before.0; self.kept_unconfirmed += after.1; self.kept_unconfirmed -= before.1; op } Err(reason) => { self.set_unresolved(prefix, Some(reason)); // Packets must not go on following a route that is no // longer the best. self.take(&prefix).map(|_| Op::Remove { prefix }) } } } /// `prefix` has no route any more. pub fn withdrawn(&mut self, prefix: IpPrefix) -> Option { self.set_unresolved(prefix, None); self.refused.remove(&prefix); self.take(&prefix).map(|_| Op::Remove { prefix }) } /// The whole table has been given: what was not in it goes. pub fn synced(&mut self) -> Vec { let stale: Vec = self .installed .iter() .filter(|(_, entry)| !entry.confirmed && !entry.kept) .map(|(prefix, _)| *prefix) .collect(); stale .into_iter() .map(|prefix| { self.take(&prefix); Op::Remove { prefix } }) .collect() } /// The kernel refused `op`. Returns what to ask of it instead, if /// anything. pub fn refused_by_kernel(&mut self, op: &Op) -> Option { match op { Op::Install { prefix, target } => { // If the plan has moved on since, this is old news. if self.installed.get(prefix)?.target != Some(*target) { return None; } self.take(prefix); self.refused.insert(*prefix); // What the kernel refused to replace, it still has. Some(Op::Remove { prefix: *prefix }) } // It was not there to remove: as wanted. Op::Remove { .. } => None, } } /// Everything goes: the changes that empty the kernel of our routes. pub fn clear(&mut self) -> Vec { self.forget_unresolved_and_refused(); self.blackholes = 0; self.kept_unconfirmed = 0; self.installed .drain() .map(|(prefix, _)| Op::Remove { prefix }) .collect() } } #[cfg(test)] mod tests { use proptest::prelude::*; use super::*; use crate::netlink::{Address, Link, Message, Nexthop, PROTOCOL_BGP, Route, TABLE_MAIN}; fn prefix(s: &str) -> IpPrefix { IpPrefix::try_from(s).unwrap() } fn target(gateway: &str, interface: u32) -> Target { Target::Via { gateway: gateway.parse().unwrap(), interface, } } fn interfaces() -> Topology { let mut topology = Topology::new(TABLE_MAIN, false); for (index, local, network) in [ (2, "192.0.2.1", "192.0.2.0/24"), (2, "2001:db8::1", "2001:db8::/64"), (3, "198.51.100.1", "198.51.100.0/24"), ] { topology.apply(&Message::Link { present: true, link: Link { index, name: format!("eth{index}"), up: true, }, }); topology.apply(&Message::Address { present: true, address: Address { interface: index, local: local.parse().unwrap(), network: prefix(network), }, }); } topology } /// A route of the kernel's main table, as a static route is. fn kernel_route(prefix_text: &str, gateway: Option<&str>, interface: Option) -> Route { Route { prefix: prefix(prefix_text), table: TABLE_MAIN, protocol: 4, metric: 0, gateway: gateway.map(|g| g.parse().unwrap()), interface, nexthop: None, kind: KIND_UNICAST, } } fn with_route(topology: &mut Topology, route: Route) { assert!(topology.apply(&Message::Route { present: true, route, })); } fn neighbor(address: &str) -> Neighbor { address.parse::().unwrap().into() } #[test] fn a_next_hop_resolves_if_it_is_a_neighbor() { let interfaces = interfaces(); let v4 = prefix("203.0.113.0/24"); let v6 = prefix("2001:db8:100::/48"); let peer = Some(neighbor("192.0.2.2")); let v4_hop = |s: &str| NlriNextHop::Ipv4(s.parse().unwrap()); let v6_hop = |s: &str| NlriNextHop::Ipv6(s.parse().unwrap()); assert_eq!( resolve(&interfaces, &v4, Some(&v4_hop("192.0.2.2")), peer, false), Ok(target("192.0.2.2", 2)) ); // A neighbor other than the peer: a route server's routes. assert_eq!( resolve(&interfaces, &v4, Some(&v4_hop("198.51.100.9")), peer, false), Ok(target("198.51.100.9", 3)) ); assert_eq!( resolve(&interfaces, &v6, Some(&v6_hop("2001:db8::2")), peer, false), Ok(target("2001:db8::2", 2)) ); for (hop, why) in [ (Some(v4_hop("10.9.9.9")), Unresolved::NoRoute), (Some(v4_hop("192.0.2.1")), Unresolved::Ourselves), (None, Unresolved::Missing), ] { assert_eq!( resolve(&interfaces, &v4, hop.as_ref(), peer, false), Err(why) ); } assert_eq!( resolve(&interfaces, &v6, Some(&v4_hop("192.0.2.2")), peer, false), Err(Unresolved::OtherFamily) ); // A route for part of a network we are on is not for the kernel, // however good its next hop; one for more than the network is. for ours in [ "192.0.2.0/24", "192.0.2.128/25", "192.0.2.7/32", "2001:db8::/64", ] { let ours = prefix(ours); let hop = match ours.address_family() { IpVersion::Ipv4 => v4_hop("192.0.2.2"), IpVersion::Ipv6 => v6_hop("2001:db8::2"), }; assert_eq!( resolve(&interfaces, &ours, Some(&hop), peer, false), Err(Unresolved::OwnNetwork), "{ours}" ); } assert_eq!( resolve( &interfaces, &prefix("192.0.0.0/22"), Some(&v4_hop("192.0.2.2")), peer, false ), Ok(target("192.0.2.2", 2)) ); // An IPv4 route by way of an IPv6 neighbor: a link with only IPv6 // addresses (RFC 8950). assert_eq!( resolve(&interfaces, &v4, Some(&v6_hop("2001:db8::2")), peer, false), Ok(target("2001:db8::2", 2)) ); assert_eq!( resolve( &interfaces, &v4, Some(&NlriNextHop::Ipv6WithLl { global: "2001:db8:ffff::2".parse().unwrap(), link_local: "fe80::2".parse().unwrap(), }), Some(neighbor("2001:db8::2")), false ), Ok(target("fe80::2", 2)) ); } #[test] fn a_link_local_next_hop_is_on_the_peers_link() { let interfaces = interfaces(); let v6 = prefix("2001:db8:100::/48"); let peer = Some(neighbor("2001:db8::2")); let link_local: Ipv6Addr = "fe80::2".parse().unwrap(); let both = |global: &str| NlriNextHop::Ipv6WithLl { global: global.parse().unwrap(), link_local, }; // The global address is a neighbor: it is used. assert_eq!( resolve(&interfaces, &v6, Some(&both("2001:db8::2")), peer, false), Ok(target("2001:db8::2", 2)) ); // It is not: the link-local one, on the interface the peer is on. assert_eq!( resolve( &interfaces, &v6, Some(&both("2001:db8:ffff::2")), peer, false ), Ok(target("fe80::2", 2)) ); assert_eq!( resolve( &interfaces, &v6, Some(&NlriNextHop::Ipv6(link_local)), peer, false ), Ok(target("fe80::2", 2)) ); // A peer that is not a neighbor says nothing about the link. let far = Some(neighbor("2001:db8:ffff::9")); assert_eq!( resolve( &interfaces, &v6, Some(&NlriNextHop::Ipv6(link_local)), far, false ), Err(Unresolved::NoRoute) ); assert_eq!( resolve(&interfaces, &v6, Some(&both("2001:db8::1")), peer, false), Err(Unresolved::Ourselves) ); } /// A session on link-local addresses: which link is known from the /// session and from nothing else, every interface having fe80::/64. #[test] fn a_link_local_peer_is_on_the_interface_of_its_session() { let mut interfaces = interfaces(); for (index, local) in [(2, "fe80::a"), (3, "fe80::b")] { interfaces.apply(&Message::Address { present: true, address: Address { interface: index, local: local.parse().unwrap(), network: prefix("fe80::/64"), }, }); } let v4 = prefix("203.0.113.0/24"); let v6 = prefix("2001:db8:100::/48"); let link_local: Ipv6Addr = "fe80::2".parse().unwrap(); let on = |interface| { Some(Neighbor { address: link_local.into(), interface, }) }; // However the peer says it: the address alone, or twice, or after // nothing where a global one would go. for next_hop in [ NlriNextHop::Ipv6(link_local), NlriNextHop::Ipv6WithLl { global: link_local, link_local, }, NlriNextHop::Ipv6WithLl { global: Ipv6Addr::UNSPECIFIED, link_local, }, ] { for interface in [2, 3] { for route in [&v4, &v6] { assert_eq!( resolve(&interfaces, route, Some(&next_hop), on(interface), false), Ok(target("fe80::2", interface)), "{next_hop:?} on {interface}" ); } } // An interface that is not there, or is down. assert_eq!( resolve(&interfaces, &v6, Some(&next_hop), on(9), false), Err(Unresolved::NoRoute) ); } // Our own address on that link, which on another is a neighbor's. let ours = NlriNextHop::Ipv6("fe80::a".parse().unwrap()); assert_eq!( resolve(&interfaces, &v6, Some(&ours), on(2), false), Err(Unresolved::Ourselves) ); assert_eq!( resolve(&interfaces, &v6, Some(&ours), on(3), false), Ok(target("fe80::a", 3)) ); // A global next hop on such a session is looked up as any other. let global = NlriNextHop::Ipv6("2001:db8::2".parse().unwrap()); assert_eq!( resolve(&interfaces, &v6, Some(&global), on(3), false), Ok(target("2001:db8::2", 2)) ); } /// A next hop that is no neighbor is reached the way the kernel's /// own routes say, and the route goes into the kernel with that way's /// gateway and interface. #[test] fn a_far_next_hop_resolves_by_the_kernels_routes() { let mut topology = interfaces(); let v4 = prefix("203.0.113.0/24"); let v6 = prefix("2001:db8:100::/48"); let peer = Some(neighbor("192.0.2.2")); let v4_hop = |s: &str| NlriNextHop::Ipv4(s.parse().unwrap()); let v6_hop = |s: &str| NlriNextHop::Ipv6(s.parse().unwrap()); let far = v4_hop("10.9.9.9"); // Nothing says the way. assert_eq!( resolve(&topology, &v4, Some(&far), peer, false), Err(Unresolved::NoRoute) ); // The default route does not count unless asked for. with_route( &mut topology, kernel_route("0.0.0.0/0", Some("192.0.2.254"), Some(2)), ); assert_eq!( resolve(&topology, &v4, Some(&far), peer, false), Err(Unresolved::OnlyDefault) ); topology.via_default = true; assert_eq!( resolve(&topology, &v4, Some(&far), peer, false), Ok(target("192.0.2.254", 2)) ); topology.via_default = false; // A static route with a gateway: the gateway is where packets go. with_route( &mut topology, kernel_route("10.9.0.0/16", Some("198.51.100.9"), Some(3)), ); assert_eq!( resolve(&topology, &v4, Some(&far), peer, false), Ok(target("198.51.100.9", 3)) ); // A more specific route onto an interface, as a tunnel's is: the // next hop itself is the neighbor there. topology.apply(&Message::Link { present: true, link: Link { index: 7, name: "wg0".into(), up: true, }, }); with_route(&mut topology, kernel_route("10.9.9.0/24", None, Some(7))); assert_eq!( resolve(&topology, &v4, Some(&far), peer, false), Ok(target("10.9.9.9", 7)) ); // The tunnel goes down: the kernel keeps such routes, we do not // use them. topology.apply(&Message::Link { present: true, link: Link { index: 7, name: "wg0".into(), up: false, }, }); assert_eq!( resolve(&topology, &v4, Some(&far), peer, false), Err(Unresolved::NoRoute) ); // A route that names a nexthop object goes where the object says. with_route( &mut topology, Route { nexthop: Some(77), ..kernel_route("10.9.9.9/32", None, None) }, ); assert_eq!( resolve(&topology, &v4, Some(&far), peer, false), Err(Unresolved::NoRoute), "an object the kernel has not told of" ); topology.apply(&Message::Nexthop { present: true, nexthop: Nexthop { id: 77, protocol: 4, gateway: Some("192.0.2.77".parse().unwrap()), interface: Some(2), }, }); assert_eq!( resolve(&topology, &v4, Some(&far), peer, false), Ok(target("192.0.2.77", 2)) ); // A route that discards packets is a way to nowhere. with_route( &mut topology, Route { kind: 6, // blackhole ..kernel_route("10.9.9.9/32", None, None) }, ); assert_eq!( resolve(&topology, &v4, Some(&far), peer, false), Err(Unresolved::Discarded) ); // Routes of BGP's never say the way, be the prefix ever so long. assert!(!topology.apply(&Message::Route { present: true, route: Route { protocol: PROTOCOL_BGP, metric: 20, ..kernel_route("10.9.9.9/32", Some("198.51.100.9"), Some(3)) }, })); // An IPv6 next hop by an IPv4 gateway is no way for an IPv6 route, // but is one for an IPv4 route (RFC 8950 the other way about is // what the kernel allows). with_route( &mut topology, kernel_route("2001:db8:9::/48", Some("198.51.100.9"), Some(3)), ); assert_eq!( resolve(&topology, &v6, Some(&v6_hop("2001:db8:9::9")), peer, false), Err(Unresolved::NoRoute) ); with_route( &mut topology, kernel_route("2001:db8:9::/48", Some("2001:db8::9"), Some(2)), ); assert_eq!( resolve(&topology, &v6, Some(&v6_hop("2001:db8:9::9")), peer, false), Ok(target("2001:db8::9", 2)) ); assert_eq!( resolve(&topology, &v4, Some(&v6_hop("2001:db8:9::9")), peer, false), Ok(target("2001:db8::9", 2)) ); // A neighbor is a neighbor, whatever the routes say. with_route( &mut topology, kernel_route("192.0.2.2/32", Some("198.51.100.9"), Some(3)), ); assert_eq!( resolve(&topology, &v4, Some(&v4_hop("192.0.2.2")), peer, false), Ok(target("192.0.2.2", 2)) ); } /// A route that asks for its packets to be dropped goes nowhere, /// whatever its next hop; one for a network of ours does not. #[test] fn a_blackhole_route_resolves_to_nowhere() { let topology = interfaces(); let far = NlriNextHop::Ipv4("10.9.9.9".parse().unwrap()); let peer = Some(neighbor("192.0.2.2")); assert_eq!( resolve( &topology, &prefix("203.0.113.9/32"), Some(&far), peer, false ), Err(Unresolved::NoRoute) ); assert_eq!( resolve(&topology, &prefix("203.0.113.9/32"), Some(&far), peer, true), Ok(Target::Discard) ); assert_eq!( resolve(&topology, &prefix("203.0.113.9/32"), None, peer, true), Ok(Target::Discard) ); assert_eq!( resolve(&topology, &prefix("192.0.2.9/32"), Some(&far), peer, true), Err(Unresolved::OwnNetwork) ); let mut plan = Plan::default(); assert_eq!( plan.best(prefix("203.0.113.9/32"), Ok(Target::Discard)), Some(Op::Install { prefix: prefix("203.0.113.9/32"), target: Target::Discard }) ); assert_eq!((plan.installed(), plan.blackholes()), (1, 1)); } #[test] fn the_kernel_is_asked_only_for_what_changes() { let mut plan = Plan::default(); let p = prefix("203.0.113.0/24"); let (a, b) = (target("192.0.2.2", 2), target("192.0.2.3", 2)); assert_eq!( plan.best(p, Ok(a)), Some(Op::Install { prefix: p, target: a }) ); // The best route changes to one with the same next hop: nothing // for the kernel. assert_eq!(plan.best(p, Ok(a)), None); assert_eq!( plan.best(p, Ok(b)), Some(Op::Install { prefix: p, target: b }) ); assert_eq!(plan.installed(), 1); // A best route that cannot be installed takes the old one out. assert_eq!( plan.best(p, Err(Unresolved::NoRoute)), Some(Op::Remove { prefix: p }) ); assert_eq!((plan.installed(), plan.unresolved()), (0, 1)); assert_eq!( plan.unresolved_by_reason().collect::>(), [(Unresolved::NoRoute, 1)] ); assert_eq!(plan.withdrawn(p), None); assert_eq!(plan.unresolved_by_reason().count(), 0); assert_eq!(plan.unresolved(), 0); plan.best(p, Ok(a)); assert_eq!(plan.withdrawn(p), Some(Op::Remove { prefix: p })); assert_eq!(plan.withdrawn(p), None); } #[test] fn what_a_whole_table_does_not_confirm_is_removed() { let mut plan = Plan::default(); let (kept, changed, stale, odd) = ( prefix("203.0.113.0/24"), prefix("198.18.0.0/15"), prefix("192.88.99.0/24"), prefix("2001:db8:5::/48"), ); let a = target("192.0.2.2", 2); // Left in the kernel by an earlier run. plan.adopt(kept, Some(a), false); plan.adopt(changed, Some(a), false); plan.adopt(stale, Some(a), false); plan.adopt(odd, None, false); assert_eq!(plan.installed(), 4); plan.reset(); // The same as the kernel has: nothing to do. assert_eq!(plan.best(kept, Ok(a)), None); let b = target("192.0.2.3", 2); assert_eq!( plan.best(changed, Ok(b)), Some(Op::Install { prefix: changed, target: b }) ); let mut removed = plan.synced(); removed.sort_by_key(|op| *op.prefix()); let mut expected = vec![Op::Remove { prefix: stale }, Op::Remove { prefix: odd }]; expected.sort_by_key(|op| *op.prefix()); assert_eq!(removed, expected); assert_eq!(plan.installed(), 2); // A second completion finds nothing more. assert_eq!(plan.synced(), []); } /// When the daemon starts its table is empty, and stays so until its /// peers have sent theirs. What an earlier run left in the kernel is /// kept meanwhile. #[test] fn routes_of_an_earlier_run_are_kept_until_released() { let mut plan = Plan::default(); let (still_right, moved, gone) = ( prefix("203.0.113.0/24"), prefix("198.18.0.0/15"), prefix("192.88.99.0/24"), ); let (a, b) = (target("192.0.2.2", 2), target("192.0.2.3", 2)); for p in [still_right, moved, gone] { plan.adopt(p, Some(a), true); } // The first whole table is the empty one: nothing goes. plan.reset(); assert_eq!(plan.synced(), []); assert_eq!((plan.installed(), plan.kept()), (3, 3)); // The peers come back. assert_eq!(plan.best(still_right, Ok(a)), None); assert_eq!( plan.best(moved, Ok(b)), Some(Op::Install { prefix: moved, target: b }) ); assert_eq!(plan.kept(), 1); // Time is up: what nobody announced again goes, and only that. assert_eq!(plan.release(), [Op::Remove { prefix: gone }]); assert_eq!((plan.installed(), plan.kept()), (2, 0)); assert_eq!(plan.release(), []); // From then on a whole table decides as always. plan.reset(); plan.best(moved, Ok(b)); assert_eq!( plan.synced(), [Op::Remove { prefix: still_right }] ); // A route that is withdrawn while kept goes at once. let mut plan = Plan::default(); plan.adopt(gone, Some(a), true); plan.best(gone, Ok(a)); assert_eq!(plan.withdrawn(gone), Some(Op::Remove { prefix: gone })); } #[test] fn a_route_the_kernel_refuses_is_not_counted_as_there() { let mut plan = Plan::default(); let p = prefix("203.0.113.0/24"); let (a, b) = (target("192.0.2.2", 2), target("192.0.2.3", 2)); let install = plan.best(p, Ok(a)).unwrap(); // What it refused to put there may have left an older one of ours. assert_eq!( plan.refused_by_kernel(&install), Some(Op::Remove { prefix: p }) ); assert_eq!((plan.installed(), plan.refused()), (0, 1)); // Refusing to remove what is not there is as wanted. assert_eq!(plan.refused_by_kernel(&Op::Remove { prefix: p }), None); // The next best route is tried afresh. assert!(plan.best(p, Ok(b)).is_some()); assert_eq!((plan.installed(), plan.refused()), (1, 0)); // News of a refusal of what has since been replaced is ignored. assert_eq!(plan.refused_by_kernel(&install), None); assert_eq!(plan.installed(), 1); let mut cleared = plan.clear(); assert_eq!(cleared.pop(), Some(Op::Remove { prefix: p })); assert_eq!(plan.installed(), 0); } /// What happens to a table of best routes. #[derive(Debug, Clone)] enum Step { Best(u8, Option), Withdrawn(u8), /// A reader that starts over: the whole table again. Resync, } fn any_step() -> impl Strategy { prop_oneof![ 4 => (0u8..12, proptest::option::weighted(0.8, 0u8..3)).prop_map(|(p, t)| Step::Best(p, t)), 2 => (0u8..12).prop_map(Step::Withdrawn), 1 => Just(Step::Resync), ] } /// What the counts would be if the entries were scanned. fn scanned(plan: &Plan) -> (usize, usize) { plan.installed .values() .map(Plan::weigh) .fold((0, 0), |(b, k), (eb, ek)| (b + eb, k + ek)) } #[test] fn the_counts_keep_step_with_the_routes() { let mut plan = Plan::default(); let p = |n: u8| prefix(&format!("203.0.113.{n}/32")); let via = target("192.0.2.2", 2); let check = |plan: &Plan| assert_eq!((plan.blackholes(), plan.kept()), scanned(plan)); // Adopted from an earlier run: two kept, one a blackhole. plan.adopt(p(1), Some(Target::Discard), true); plan.adopt(p(2), Some(via), true); plan.adopt(p(3), Some(via), false); check(&plan); // Heard of again, as a blackhole and as a route by way of a // gateway; one changes kind. plan.best(p(1), Ok(Target::Discard)); plan.best(p(2), Ok(Target::Discard)); plan.best(p(4), Ok(Target::Discard)); check(&plan); plan.best(p(4), Ok(via)); check(&plan); // The whole table again: everything unconfirmed, then confirmed. plan.reset(); check(&plan); plan.best(p(1), Ok(Target::Discard)); plan.synced(); check(&plan); plan.reset(); plan.release(); check(&plan); // Gone for want of a next hop, withdrawn, refused, adopted over. plan.best(p(1), Err(Unresolved::Missing)); plan.best(p(5), Ok(Target::Discard)); plan.withdrawn(p(5)); plan.best(p(6), Ok(Target::Discard)); plan.refused_by_kernel(&Op::Install { prefix: p(6), target: Target::Discard, }); plan.adopt(p(7), Some(Target::Discard), true); plan.adopt(p(7), Some(via), true); check(&plan); let had = plan.installed(); assert_eq!(plan.clear().len(), had); assert_eq!((plan.blackholes(), plan.kept()), (0, 0)); } proptest! { /// Whatever happens to the best routes, and whatever an earlier /// run left behind, a kernel that does as it is asked ends up with /// exactly the best routes that resolve. #[test] fn the_kernel_ends_up_with_the_best_routes_that_resolve( left_behind in proptest::collection::vec((0u8..16, 0u8..3), 0..6), steps in proptest::collection::vec(any_step(), 0..60) ) { let p = |n: u8| IpPrefix::new(IpVersion::Ipv4, [10, n, 0, 0], 16).unwrap(); let t = |n: u8| target(&format!("192.0.2.{}", n + 2), 2); let mut kernel: HashMap = HashMap::default(); let mut plan = Plan::default(); for (prefix, gateway) in left_behind { kernel.insert(p(prefix), t(gateway)); plan.adopt(p(prefix), Some(t(gateway)), false); } // The best routes: the prefix's next hop, if it resolves. let mut table: HashMap> = HashMap::default(); let apply = |kernel: &mut HashMap, op: Option| match op { Some(Op::Install { prefix, target }) => { kernel.insert(prefix, target); } Some(Op::Remove { prefix }) => { kernel.remove(&prefix); } None => {} }; let resolved = |gateway: Option| gateway.map(t).ok_or(Unresolved::NoRoute); // A new reader is first given the whole table. let steps = std::iter::once(Step::Resync).chain(steps); for step in steps { match step { Step::Best(prefix, gateway) => { table.insert(prefix, gateway); apply(&mut kernel, plan.best(p(prefix), resolved(gateway))); } Step::Withdrawn(prefix) => { table.remove(&prefix); apply(&mut kernel, plan.withdrawn(p(prefix))); } Step::Resync => { plan.reset(); for (prefix, gateway) in &table { apply(&mut kernel, plan.best(p(*prefix), resolved(*gateway))); } for op in plan.synced() { apply(&mut kernel, Some(op)); } } } let wanted: HashMap = table .iter() .filter_map(|(prefix, gateway)| Some((p(*prefix), t((*gateway)?)))) .collect(); prop_assert_eq!(&kernel, &wanted); prop_assert_eq!(plan.installed(), wanted.len()); prop_assert_eq!(plan.unresolved(), table.len() - wanted.len()); prop_assert_eq!( plan.unresolved_by_reason().map(|(_, count)| count).sum::(), plan.unresolved() ); } } } }