//! Import policy: which of a peer's routes are taken. //! //! A peer can announce anything. What it announces by mistake or on purpose //! that is not its to announce becomes, if we take it, a route we use and //! pass on. So every route is judged before it goes into the routing table, //! by rules that are part of each peer's configuration: //! //! - **Checks that hold for any peer**, on unless turned off: no prefixes //! that are not routed on the Internet, none more specific than is routed //! between networks, no reserved AS numbers in the path, a path that //! begins with the peer's own AS and is not absurdly long. //! - **What this peer in particular may announce**, if configured: a list //! of prefixes, and the AS numbers routes may originate from. This is //! what is generated from the routing registries for each customer or //! exchange member. //! - **How many routes** the peer may have at a time. One more ends the //! session, and it is kept down for a while: a peer that suddenly sends //! a great many more routes than it should is leaking someone else's. //! //! A route with our own AS number in its path is never taken, whatever is //! configured: it has been through this AS already (RFC 4271 §9.1.2). //! //! A route that is refused is not kept. If the peer had announced the same //! route before in a form that was taken, that one goes: the peer has //! replaced it, and the replacement is not acceptable. use std::collections::{HashMap, HashSet}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::sync::{Arc, LazyLock, Mutex}; use std::time::{Duration, Instant}; use bgp_packet::message::{AS_TRANS, AsPathAttribute, NlriNextHop, PathAttribute}; use bgp_packet::open::BgpRole; use bgp_primitives::constants::IpVersion; use bgp_primitives::ip_prefix::IpPrefix; use crate::address::PeerAddress; use crate::sync::IgnorePoison; /// The longest prefixes routed between networks: anything more specific is /// filtered nearly everywhere, and is usually a leak of internal routes. const LONGEST_IPV4: u8 = 24; const LONGEST_IPV6: u8 = 48; /// The shortest prefixes anyone announces, short of a default route. const SHORTEST_IPV4: u8 = 8; const SHORTEST_IPV6: u8 = 16; /// A path longer than this is not one traffic would be sent along; it is a /// mistake or an attempt to exhaust something. const DEFAULT_MAX_AS_PATH_LENGTH: u16 = 64; /// How long a peer that went over its limit is kept down, unless /// configured otherwise. const DEFAULT_HOLD_DOWN: Duration = Duration::from_secs(600); /// What is taken from one peer. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ImportPolicy { /// The most routes the peer may have at a time; `None` for no limit. pub max_prefixes: Option, /// How long the peer is kept down after going over the limit. pub hold_down: Duration, /// Take prefixes and AS numbers that are reserved, private or for /// documentation. pub accept_bogons: bool, pub accept_default_route: bool, /// The longest IPv4 and IPv6 prefixes taken. pub max_ipv4_length: u8, pub max_ipv6_length: u8, /// Take routes whose path does not begin with the peer's AS. A route /// server does not put its own AS on the path (RFC 7947 §2.2.2.1). pub accept_any_first_as: bool, /// The longest AS path taken, counted in AS numbers. pub max_as_path_length: u16, /// The prefixes the peer may announce; `None` for any. pub prefixes: Option, /// The AS numbers the peer's routes may originate from; `None` for /// any. pub origin_asns: Option>, /// Take routes that route origin validation finds invalid (RFC 6811). pub accept_rpki_invalid: bool, /// Take routes whose path the ASPAs find invalid. pub accept_aspa_invalid: bool, /// Take routes whose path has an AS_SET in it. They are deprecated /// (RFC 9774): with one, nobody can say where a route comes from. pub accept_as_sets: bool, /// Take routes whose next hop is not the peer itself. Without this a /// peer can only draw our traffic to itself, not send it to someone /// who never asked for it. A route server's routes always have its /// clients as next hops, and are taken either way. pub accept_third_party_next_hop: bool, /// The prefixes this router originates. Those, and what is inside /// them, are ours and not taken from another network, unless /// `prefixes` says in so many words that this peer may announce them: /// a customer with a part of our space. Empty for a peer in our own /// AS. pub own_prefixes: Arc, } impl Default for ImportPolicy { /// What holds for a peer with nothing configured: every general check, /// no list of its own, no limit. fn default() -> Self { Self { max_prefixes: None, hold_down: DEFAULT_HOLD_DOWN, accept_bogons: false, accept_default_route: false, max_ipv4_length: LONGEST_IPV4, max_ipv6_length: LONGEST_IPV6, accept_any_first_as: false, max_as_path_length: DEFAULT_MAX_AS_PATH_LENGTH, prefixes: None, origin_asns: None, accept_rpki_invalid: false, accept_aspa_invalid: false, accept_as_sets: false, accept_third_party_next_hop: false, own_prefixes: Arc::default(), } } } impl ImportPolicy { /// Takes everything but routes that have been through our own AS. For /// a closed test network, where the prefixes and AS numbers are the /// private and documentation ones a real policy refuses. pub fn permissive() -> Self { Self { accept_bogons: true, accept_default_route: true, max_ipv4_length: IpVersion::Ipv4.bits(), max_ipv6_length: IpVersion::Ipv6.bits(), accept_any_first_as: true, max_as_path_length: u16::MAX, accept_rpki_invalid: true, accept_aspa_invalid: true, accept_as_sets: true, accept_third_party_next_hop: true, ..Self::default() } } } /// Why a route was not taken. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Rejection { /// The prefix is reserved, private or for documentation. BogonPrefix, /// A default route, or a prefix too short to be anyone's. TooShort, /// The prefix is more specific than is routed between networks. TooSpecific, /// The prefix is not one the peer is allowed to announce. PrefixNotAllowed, /// The path holds our own AS number: the route has been here before. OwnAs, /// The path holds an AS number that is reserved, private or for /// documentation. BogonAs, /// The path does not begin with the peer's AS number. FirstAs, PathTooLong, /// The route does not originate from an AS the peer may announce /// routes of. OriginNotAllowed, /// The holder of the addresses has said who may originate routes for /// them, and it is not this route's origin (RFC 6811). RpkiInvalid, /// A network on the path has said who its providers are, and the path /// does not go by them: a route leak, or a path that was made up. AspaInvalid, /// The route is marked as one that may only be passed down to /// customers, and it did not come from above: someone has leaked it /// (RFC 9234). Leak, /// The path has an AS_SET in it (RFC 9774). AsSet, /// The next hop is no address packets could be sent to, or is our /// own, or is an IPv6 address for an IPv4 route on a session that did /// not agree to that (RFC 8950). NextHopUnusable, /// The next hop is not the peer. NextHopNotPeer, /// The prefix is one of ours, or inside one. OwnPrefix, /// The next hop cannot be reached from this machine as its interfaces /// are now, so the kernel could not be given the route. NextHopUnreachable, /// The operator's policy function for the peer refused the route. Policy, /// The operator's policy function failed on the route, which is as /// good as refusing it. PolicyFailed, } impl Rejection { /// Whether the reason may pass by itself, with the route unchanged: /// the RPKI's data changes, and so do the machine's interfaces. A /// route refused for such a reason is held, to be taken when it does. pub(crate) fn may_pass(self) -> bool { matches!( self, Self::RpkiInvalid | Self::AspaInvalid | Self::NextHopUnreachable ) } pub const ALL: [Self; 19] = [ Self::BogonPrefix, Self::TooShort, Self::TooSpecific, Self::PrefixNotAllowed, Self::OwnAs, Self::BogonAs, Self::FirstAs, Self::PathTooLong, Self::OriginNotAllowed, Self::RpkiInvalid, Self::AspaInvalid, Self::Leak, Self::AsSet, Self::NextHopUnusable, Self::NextHopNotPeer, Self::OwnPrefix, Self::NextHopUnreachable, Self::Policy, Self::PolicyFailed, ]; /// Where the reason is in [`Rejection::ALL`]. pub(crate) fn index(self) -> usize { self as usize } pub fn as_str(self) -> &'static str { match self { Self::BogonPrefix => "bogon prefix", Self::TooShort => "prefix too short", Self::TooSpecific => "prefix too specific", Self::PrefixNotAllowed => "prefix not allowed", Self::OwnAs => "own AS in path", Self::BogonAs => "bogon AS in path", Self::FirstAs => "path does not begin with the peer's AS", Self::PathTooLong => "path too long", Self::OriginNotAllowed => "origin AS not allowed", Self::RpkiInvalid => "RPKI invalid", Self::AspaInvalid => "ASPA invalid", Self::Leak => "route leak (Only to Customer)", Self::AsSet => "AS_SET in path", Self::NextHopUnusable => "next hop unusable", Self::NextHopNotPeer => "next hop is not the peer", Self::OwnPrefix => "prefix is our own", Self::NextHopUnreachable => "next hop unreachable", Self::Policy => "refused by policy function", Self::PolicyFailed => "policy function failed", } } } /// What a peer is to us, in the business between networks. It decides /// what its routes may look like: a customer's routes have come up to us /// through its own customers only, while a provider's may have been /// anywhere. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum PeerRole { /// We buy transit from the peer. Provider, /// The peer buys transit from us. Customer, /// The peer and we exchange our own and our customers' routes. Peer, /// The peer is an Internet exchange's route server, and we are one of /// its clients. RouteServer, /// We are a route server, and the peer is one of our clients. RouteServerClient, } impl PeerRole { /// The role the peer has to announce in its OPEN: its own (RFC 9234 /// §4.1). pub fn theirs(self) -> BgpRole { match self { Self::Provider => BgpRole::Provider, Self::Customer => BgpRole::Customer, Self::Peer => BgpRole::Peer, Self::RouteServer => BgpRole::RouteServer, Self::RouteServerClient => BgpRole::RouteServerClient, } } /// The role we announce in ours: what we are to the peer, which is /// the other half of the pair (RFC 9234 §4.2). pub fn ours(self) -> BgpRole { match self { Self::Provider => BgpRole::Customer, Self::Customer => BgpRole::Provider, Self::Peer => BgpRole::Peer, Self::RouteServer => BgpRole::RouteServerClient, Self::RouteServerClient => BgpRole::RouteServer, } } /// Whether the peer is above or beside us, and not below: a provider, /// a peer, or a route server. A route that came from there may only /// go on to customers. fn is_above_or_beside(self) -> bool { matches!(self, Self::Provider | Self::Peer | Self::RouteServer) } /// The Only to Customer attribute to add to a route received from the /// peer, whose AS number is `peer_asn`, if the route does not have /// one (RFC 9234 §5, ingress, 3). pub fn only_to_customer_on_import(self, peer_asn: u32) -> Option { self.is_above_or_beside().then_some(peer_asn) } /// The Only to Customer attribute to add to a route sent to the peer /// if the route does not have one (RFC 9234 §5, egress, 1): the route /// is going down or across, and may not come up again. pub fn only_to_customer_on_export(self, local_asn: u32) -> Option { matches!(self, Self::Customer | Self::Peer | Self::RouteServerClient).then_some(local_asn) } /// Whether a route with an Only to Customer attribute may be sent to /// the peer: not upwards or across (RFC 9234 §5, egress, 2). pub fn may_be_sent_only_to_customer_routes(self) -> bool { !self.is_above_or_beside() } pub fn as_str(self) -> &'static str { match self { Self::Provider => "provider", Self::Customer => "customer", Self::Peer => "peer", Self::RouteServer => "route server", Self::RouteServerClient => "route server client", } } /// The role as a policy script has it in `peer.role`: one word. pub fn in_scripts(self) -> &'static str { match self { Self::Provider => "provider", Self::Customer => "customer", Self::Peer => "peer", Self::RouteServer => "route_server", Self::RouteServerClient => "route_server_client", } } } /// Whose routes are being judged. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RouteSource { pub peer_asn: u32, pub local_asn: u32, /// Whether the peer is in our own AS. The checks on the path are about /// routes coming in from other networks, and are not made then. pub ibgp: bool, /// Whether AS numbers are four octets wide on the session. If not, a /// path may hold AS_TRANS where a wider number belongs. pub four_octet_asn: bool, /// What the peer is to us, if that is known. pub role: Option, /// The two ends of the session. pub peer_address: IpAddr, pub local_address: IpAddr, /// Whether IPv4 routes may come with IPv6 next hops (RFC 8950). pub extended_next_hop: bool, } impl ImportPolicy { /// Judges what the routes of one UPDATE have in common: their path. /// If it passes, returns the AS the routes originate from: the last AS /// of the path, or `None` if the path is empty or ends in a set. pub fn check_path( &self, attributes: &[PathAttribute], from: &RouteSource, ) -> Result, Rejection> { let path = attributes.iter().find_map(|attribute| match attribute { PathAttribute::AsPath(path) => Some(path), _ => None, }); let facts = PathFacts::of(path, from); // The origin is the last AS of the path. If the path ends in a // set, there is no telling which of its members that is. let ends_in_sequence = path .and_then(|path| path.segments.last()) .is_some_and(|segment| segment.ordered); let origin = facts.last.filter(|_| ends_in_sequence); if from.ibgp { return Ok(origin); } if facts.has_local_asn { return Err(Rejection::OwnAs); } // RFC 9234 §5, ingress, 1 and 2. A route that is only to go to // customers has been down or across once already. From a customer // it is coming up again; from a peer it is coming across again, // unless it was the peer itself that marked it, on its way to us. let only_to_customer = attributes.iter().find_map(|attribute| match attribute { PathAttribute::OnlyToCustomer(asn) => Some(*asn), _ => None, }); if let (Some(marked_by), Some(role)) = (only_to_customer, from.role) { let leak = match role { PeerRole::Customer | PeerRole::RouteServerClient => true, PeerRole::Peer => marked_by != from.peer_asn, PeerRole::Provider | PeerRole::RouteServer => false, }; if leak { return Err(Rejection::Leak); } } if !self.accept_any_first_as { // The first AS of the first segment, which has to be a // sequence: a set has no first. let first = path .and_then(|path| path.segments.first()) .filter(|segment| segment.ordered) .and_then(|segment| segment.path.first()); if first != Some(&from.peer_asn) { return Err(Rejection::FirstAs); } } if facts.length > usize::from(self.max_as_path_length) { return Err(Rejection::PathTooLong); } if !self.accept_as_sets && facts.has_set { return Err(Rejection::AsSet); } if !self.accept_bogons && facts.has_bogon_asn { return Err(Rejection::BogonAs); } if let Some(allowed) = &self.origin_asns && !origin.is_some_and(|origin| allowed.contains(&origin)) { return Err(Rejection::OriginNotAllowed); } Ok(origin) } /// Judges one prefix. pub fn check_prefix(&self, prefix: &IpPrefix) -> Result<(), Rejection> { let (shortest, longest) = match prefix.address_family() { IpVersion::Ipv4 => (SHORTEST_IPV4, self.max_ipv4_length), IpVersion::Ipv6 => (SHORTEST_IPV6, self.max_ipv6_length), }; if prefix.length() > longest { return Err(Rejection::TooSpecific); } if prefix.length() == 0 { if !self.accept_default_route { return Err(Rejection::TooShort); } } else if !self.accept_bogons { if prefix.length() < shortest { return Err(Rejection::TooShort); } if is_bogon_prefix(prefix) { return Err(Rejection::BogonPrefix); } } match &self.prefixes { Some(allowed) if !allowed.allows(prefix) => Err(Rejection::PrefixNotAllowed), // Said to be this peer's to announce, ours or not. Some(_) => Ok(()), None if self.own_prefixes.allows(prefix) => Err(Rejection::OwnPrefix), None => Ok(()), } } /// Judges the next hop of routes of IP version `routes`. /// /// It has to be an address packets can be sent to and not our own /// (RFC 4271 §5.1.3), and of a kind the session agreed on. From /// another network it also has to be the peer itself, unless the /// policy says otherwise: a next hop is where our traffic will go, /// and a peer is not to send it to a third party, whether another /// member of an exchange or a machine on some other network of ours. pub fn check_next_hop( &self, next_hop: &NlriNextHop, routes: IpVersion, from: &RouteSource, ) -> Result<(), Rejection> { let (first, link_local): (IpAddr, Option) = match *next_hop { NlriNextHop::Ipv4(address) => (address.into(), None), NlriNextHop::Ipv6(address) => (address.into(), None), NlriNextHop::Ipv6WithLl { global, link_local } => (global.into(), Some(link_local)), }; match (routes, first) { (IpVersion::Ipv4, IpAddr::V4(_)) | (IpVersion::Ipv6, IpAddr::V6(_)) => {} (IpVersion::Ipv4, IpAddr::V6(_)) if from.extended_next_hop => {} _ => return Err(Rejection::NextHopUnusable), } // With a link-local address after it, the first may be nothing at // all, or the same once more, from a peer that has no global one. let filler = link_local .is_some_and(|link_local| first.is_unspecified() || first == IpAddr::V6(link_local)); let named = (!filler) .then_some(first) .into_iter() .chain(link_local.map(IpAddr::V6)); let mut is_peer = false; for address in named { let usable = !address.is_unspecified() && !address.is_loopback() && !address.is_multicast() && address != IpAddr::V4(Ipv4Addr::BROADCAST) && address != from.local_address; if !usable { return Err(Rejection::NextHopUnusable); } is_peer |= address == from.peer_address; } if link_local.is_some_and(|link_local| !link_local.is_unicast_link_local()) { return Err(Rejection::NextHopUnusable); } // Whether the next hop is the peer can be told only if both are // addresses of the same kind: IPv6 routes over an IPv4 session // have a next hop the session knows nothing of. let comparable = first.is_ipv4() == from.peer_address.is_ipv4(); let third_party_allowed = from.ibgp || self.accept_third_party_next_hop || from.role == Some(PeerRole::RouteServer); if comparable && !is_peer && !third_party_allowed { return Err(Rejection::NextHopNotPeer); } Ok(()) } } /// What the checks need to know of a path's AS numbers, found in one pass /// over them and without copying the path: this is done for every UPDATE. struct PathFacts { /// How many AS numbers the path holds, sets and all. length: usize, has_local_asn: bool, has_bogon_asn: bool, has_set: bool, last: Option, } impl PathFacts { /// Reads the path. It is the whole path: what a session with /// two-octet AS numbers brought in AS4_PATH the decoder has put in its /// place (RFC 6793 §4.2.3). fn of(path: Option<&AsPathAttribute>, from: &RouteSource) -> Self { let mut facts = Self { length: 0, has_local_asn: false, has_bogon_asn: false, has_set: path.is_some_and(|path| path.segments.iter().any(|segment| !segment.ordered)), last: None, }; for asn in asns(path) { facts.length += 1; facts.has_local_asn |= asn == from.local_asn; facts.has_bogon_asn |= is_bogon_asn(asn, from.four_octet_asn); facts.last = Some(asn); } facts } } /// Puts the AS numbers of the path that `attributes` describe in `out`, /// in place of what was there, as [`PathFacts`] reads it: the neighbor's /// first, the origin's last, sets and all. Returns whether the path has a /// set in it. pub(crate) fn effective_path(attributes: &[PathAttribute], out: &mut Vec) -> bool { let path = attributes.iter().find_map(|attribute| match attribute { PathAttribute::AsPath(path) => Some(path), _ => None, }); out.clear(); out.extend(asns(path)); path.into_iter() .flat_map(|path| &path.segments) .any(|segment| !segment.ordered) } /// The AS numbers of a path in order, sets and all. pub(crate) fn asns(path: Option<&AsPathAttribute>) -> impl Iterator + '_ { path.into_iter() .flat_map(|path| &path.segments) .flat_map(|segment| segment.path.iter().copied()) } /// AS numbers that are not anyone's on the Internet: reserved, private use /// (RFC 6996), for documentation (RFC 5398), or AS_TRANS where a real /// number should be. fn is_bogon_asn(asn: u32, four_octet_asn: bool) -> bool { // On a two-octet session AS_TRANS stands for a number that did not // fit, and is in the path by rights. if asn == u32::from(AS_TRANS) { return four_octet_asn; } matches!( asn, 0 | 64496..=131071 | 4_200_000_000..=u32::MAX ) } /// Address space that is not routed on the Internet. A prefix inside any of /// these is a bogon. static BOGONS: LazyLock> = LazyLock::new(|| { [ // IPv4 (RFC 6890 and its successors). "0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.2.0/24", "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4", // IPv6 inside the global unicast space. "2001::/32", "2001:2::/48", "2001:10::/28", "2001:20::/28", "2001:db8::/32", "2002::/16", "3ffe::/16", "3fff::/20", ] .into_iter() .map(|prefix| IpPrefix::try_from(prefix).expect("the bogon list is written correctly")) .collect() }); /// The only IPv6 space that is routed: global unicast. static GLOBAL_UNICAST_V6: LazyLock = LazyLock::new(|| IpPrefix::try_from("2000::/3").expect("a prefix")); fn is_bogon_prefix(prefix: &IpPrefix) -> bool { // Nearly every prefix is nowhere near a bogon, and this is asked for // every route: the address's first octets say whether the list needs // looking at. A prefix too short to have them is looked up in full. let may_be_listed = match (prefix.address_family(), prefix.bytes()) { (IpVersion::Ipv4, [first, ..]) => { matches!( first, 0 | 10 | 100 | 127 | 169 | 172 | 192 | 198 | 203 | 224..=255 ) } (IpVersion::Ipv6, [first, second, ..]) => { if first & 0xe0 != 0x20 { // Outside 2000::/3. return true; } matches!((first, second), (0x20, 0x01 | 0x02) | (0x3f, 0xfe | 0xff)) } _ => true, }; may_be_listed && is_listed_bogon(prefix) } /// Whether `prefix` is in space that is not routed, by looking through the /// whole list. fn is_listed_bogon(prefix: &IpPrefix) -> bool { if prefix.address_family() == IpVersion::Ipv6 && !GLOBAL_UNICAST_V6.contains(prefix) { return true; } BOGONS.iter().any(|bogon| bogon.contains(prefix)) } /// A list of prefixes a peer may announce. Each entry allows the prefix /// itself and, if it says so, the more specific prefixes inside it up to a /// length. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct PrefixSet { /// For each prefix listed, the longest prefix inside it that is /// allowed. entries: HashMap, /// The lengths of the prefixes listed, by IP version: the only lengths /// at which a covering entry can be. lengths: [Vec; 2], } impl PrefixSet { /// Allows `prefix` and every prefix inside it of at most `max_length` /// bits. pub fn allow(&mut self, prefix: IpPrefix, max_length: u8) { let longest = self.entries.entry(prefix).or_insert(max_length); *longest = (*longest).max(max_length); let lengths = &mut self.lengths[Self::version_index(&prefix)]; if let Err(at) = lengths.binary_search(&prefix.length()) { lengths.insert(at, prefix.length()); } } pub fn len(&self) -> usize { self.entries.len() } pub fn is_empty(&self) -> bool { self.entries.is_empty() } /// Whether `prefix` is allowed by some entry. pub fn allows(&self, prefix: &IpPrefix) -> bool { // An entry that covers it is the prefix cut to the entry's length, // so there is one place to look for each length there is. self.lengths[Self::version_index(prefix)] .iter() .take_while(|length| **length <= prefix.length()) .any(|length| { self.entries .get(&prefix.shortened(*length)) .is_some_and(|longest| prefix.length() <= *longest) }) } fn version_index(prefix: &IpPrefix) -> usize { match prefix.address_family() { IpVersion::Ipv4 => 0, IpVersion::Ipv6 => 1, } } } /// Why a peer is being kept down. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HeldDown { /// It went over its limit of routes. OverLimit, /// Its sessions keep coming up and failing. Flapping, } impl HeldDown { pub fn as_str(self) -> &'static str { match self { Self::OverLimit => "held down after going over its limit of routes", Self::Flapping => "held down because its sessions keep failing", } } } /// A session that fails sooner than this after coming up counts as a /// flap. const FLAP_WITHIN: Duration = Duration::from_secs(60); /// This many flaps in a row are let pass: a router being restarted, a /// cable put back. const FLAPS_ALLOWED: u32 = 2; /// How long a peer is kept down after the first flap too many; twice as /// long after each one more, up to `FLAP_HOLD_MAX`. const FLAP_HOLD: Duration = Duration::from_secs(1); const FLAP_HOLD_MAX: Duration = Duration::from_secs(300); /// Peers that are being kept down, and until when: those that went over /// their limit, and those whose sessions keep failing. Shared by the /// sessions, which put peers here, and by the dialer and the listeners, /// which leave them alone meanwhile. /// /// Every session that comes up and goes costs a look at every route in /// the table, and tells every other peer of the changes. A peer that /// does so over and over, by fault or by design, is slowed down. #[derive(Debug, Default)] pub struct HoldDowns { held: Mutex>, /// How many times in a row each peer's session has failed soon after /// coming up. flaps: Mutex>, } impl HoldDowns { /// Keeps the peer at `address` down for `duration` from now. pub fn hold(&self, address: PeerAddress, duration: Duration, why: HeldDown) { let until = Instant::now() + duration; let mut held = self.held.locked(); // Not for less long than it already is. if held .get(&address) .is_none_or(|(already, _)| *already < until) { held.insert(address, (until, why)); } } /// How much longer the peer at `address` is kept down, if it is, and /// why. pub fn remaining(&self, address: PeerAddress) -> Option<(Duration, HeldDown)> { let mut held = self.held.locked(); let (until, why) = *held.get(&address)?; let remaining = until.saturating_duration_since(Instant::now()); if remaining.is_zero() { held.remove(&address); return None; } Some((remaining, why)) } /// A session with the peer at `address` that had come up has ended, /// `lasted` after it did; `failed` if not at our own wish. Returns for /// how long the peer is kept down for it, if it is. pub fn session_ended( &self, address: PeerAddress, lasted: Duration, failed: bool, ) -> Option { let mut flaps = self.flaps.locked(); if !failed || lasted >= FLAP_WITHIN { flaps.remove(&address); return None; } let count = flaps.entry(address).or_insert(0); *count += 1; let over = count.checked_sub(FLAPS_ALLOWED + 1)?; let hold = FLAP_HOLD .checked_mul(1u32.checked_shl(over).unwrap_or(u32::MAX)) .map_or(FLAP_HOLD_MAX, |hold| hold.min(FLAP_HOLD_MAX)); drop(flaps); self.hold(address, hold, HeldDown::Flapping); Some(hold) } } #[cfg(test)] mod tests { use bgp_packet::message::AsPathSegment; use proptest::prelude::*; use super::*; fn prefix(s: &str) -> IpPrefix { IpPrefix::try_from(s).unwrap() } const EXTERNAL: RouteSource = RouteSource { peer_asn: 3333, local_asn: 1299, ibgp: false, four_octet_asn: true, role: None, peer_address: IpAddr::V4(Ipv4Addr::new(192, 0, 2, 2)), local_address: IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)), extended_next_hop: false, }; fn path(asns: &[u32]) -> Vec { vec![AsPathAttribute::from_asns(asns.to_vec())] } #[test] fn prefixes_that_are_not_routed_on_the_internet_are_refused() { let policy = ImportPolicy::default(); for bogon in [ "10.1.0.0/16", "192.168.1.0/24", "100.64.0.0/10", "192.0.2.0/24", "224.0.0.0/8", "2001:db8::/32", "2001:db8:1::/48", "fc00::/16", "fe80::/16", "2002::/16", ] { assert_eq!( policy.check_prefix(&prefix(bogon)), Err(Rejection::BogonPrefix), "{bogon}" ); } for routed in [ "193.0.0.0/21", "8.8.8.0/24", "2001:67c:2e8::/48", "2a00::/16", ] { assert_eq!(policy.check_prefix(&prefix(routed)), Ok(()), "{routed}"); } // The space around a bogon is not one. assert_eq!(policy.check_prefix(&prefix("192.0.3.0/24")), Ok(())); assert_eq!(policy.check_prefix(&prefix("11.0.0.0/8")), Ok(())); } #[test] fn prefixes_too_long_or_too_short_are_refused() { let policy = ImportPolicy::default(); assert_eq!( policy.check_prefix(&prefix("193.0.0.0/25")), Err(Rejection::TooSpecific) ); assert_eq!( policy.check_prefix(&prefix("2001:67c:2e8::/49")), Err(Rejection::TooSpecific) ); assert_eq!( policy.check_prefix(&prefix("0.0.0.0/0")), Err(Rejection::TooShort) ); assert_eq!( policy.check_prefix(&prefix("::/0")), Err(Rejection::TooShort) ); assert_eq!( policy.check_prefix(&prefix("128.0.0.0/1")), Err(Rejection::TooShort) ); // Each can be allowed for a peer it is expected from. let upstream = ImportPolicy { accept_default_route: true, max_ipv4_length: 32, ..ImportPolicy::default() }; assert_eq!(upstream.check_prefix(&prefix("0.0.0.0/0")), Ok(())); assert_eq!(upstream.check_prefix(&prefix("193.0.0.1/32")), Ok(())); // And nothing else is let through with them. assert_eq!( upstream.check_prefix(&prefix("10.0.0.1/32")), Err(Rejection::BogonPrefix) ); } #[test] fn a_peer_with_a_prefix_list_may_announce_only_what_is_on_it() { let mut allowed = PrefixSet::default(); allowed.allow(prefix("193.0.0.0/21"), 21); allowed.allow(prefix("2001:67c::/32"), 48); let policy = ImportPolicy { prefixes: Some(allowed), ..ImportPolicy::default() }; assert_eq!(policy.check_prefix(&prefix("193.0.0.0/21")), Ok(())); // More specific than the entry allows. assert_eq!( policy.check_prefix(&prefix("193.0.0.0/24")), Err(Rejection::PrefixNotAllowed) ); // Less specific than the entry. assert_eq!( policy.check_prefix(&prefix("193.0.0.0/20")), Err(Rejection::PrefixNotAllowed) ); assert_eq!(policy.check_prefix(&prefix("2001:67c:2e8::/48")), Ok(())); assert_eq!(policy.check_prefix(&prefix("2001:67c::/32")), Ok(())); // Someone else's. assert_eq!( policy.check_prefix(&prefix("8.8.8.0/24")), Err(Rejection::PrefixNotAllowed) ); // The general checks still hold for what is on the list. assert_eq!( policy.check_prefix(&prefix("2001:67c:2e8:1::/64")), Err(Rejection::TooSpecific) ); } #[test] fn a_route_that_has_been_through_our_as_is_never_taken() { // Not even by a policy that takes everything else. for policy in [ImportPolicy::default(), ImportPolicy::permissive()] { assert_eq!( policy.check_path(&path(&[3333, 1299, 2914]), &EXTERNAL), Err(Rejection::OwnAs) ); } // From a peer in our own AS the path says nothing of the kind. let internal = RouteSource { peer_asn: 1299, ibgp: true, ..EXTERNAL }; assert_eq!( ImportPolicy::default() .check_path(&path(&[]), &internal) .map(drop), Ok(()) ); } #[test] fn the_path_must_begin_with_the_peers_as_unless_it_is_a_route_server() { let policy = ImportPolicy::default(); assert_eq!( policy.check_path(&path(&[3333, 2914]), &EXTERNAL).map(drop), Ok(()) ); assert_eq!( policy.check_path(&path(&[2914, 3333]), &EXTERNAL), Err(Rejection::FirstAs) ); assert_eq!( policy.check_path(&path(&[]), &EXTERNAL), Err(Rejection::FirstAs) ); // A set has no first AS. let set = vec![PathAttribute::AsPath(AsPathAttribute { segments: vec![AsPathSegment { ordered: false, path: vec![3333], }], })]; assert_eq!(policy.check_path(&set, &EXTERNAL), Err(Rejection::FirstAs)); let route_server = ImportPolicy { accept_any_first_as: true, ..ImportPolicy::default() }; assert_eq!( route_server .check_path(&path(&[2914, 174]), &EXTERNAL) .map(drop), Ok(()) ); } #[test] fn reserved_and_private_as_numbers_in_the_path_are_refused() { let policy = ImportPolicy::default(); for bogon in [ 0, 23456, 64496, 64512, 65000, 65535, 65536, 131071, 4_200_000_000, u32::MAX, ] { assert_eq!( policy.check_path(&path(&[3333, bogon]), &EXTERNAL), Err(Rejection::BogonAs), "AS{bogon}" ); } for real in [174, 64495, 131072, 210036, 4_199_999_999] { assert_eq!( policy.check_path(&path(&[3333, real]), &EXTERNAL).map(drop), Ok(()), "AS{real}" ); } } /// RFC 9234 §5, the ingress procedure. #[test] fn a_route_only_for_customers_is_a_leak_from_below_or_from_a_second_peer() { let policy = ImportPolicy::default(); let marked_by = |asn: u32| { let mut attributes = path(&[3333, 2121]); attributes.push(PathAttribute::OnlyToCustomer(asn)); attributes }; let from = |role| RouteSource { role: Some(role), ..EXTERNAL }; // From below it is always a leak. for role in [PeerRole::Customer, PeerRole::RouteServerClient] { for asn in [3333, 2121] { assert_eq!( policy.check_path(&marked_by(asn), &from(role)), Err(Rejection::Leak), "{role:?}" ); } } // From a peer, unless the peer marked it itself. assert_eq!( policy.check_path(&marked_by(2121), &from(PeerRole::Peer)), Err(Rejection::Leak) ); assert_eq!( policy.check_path(&marked_by(3333), &from(PeerRole::Peer)), Ok(Some(2121)) ); // From above it is where it should be. for role in [PeerRole::Provider, PeerRole::RouteServer] { assert_eq!( policy.check_path(&marked_by(2121), &from(role)), Ok(Some(2121)), "{role:?}" ); } // Without the mark nothing is known, whoever it is from; and // without knowing what the peer is, the mark says nothing. for role in [PeerRole::Customer, PeerRole::Peer, PeerRole::Provider] { assert_eq!( policy.check_path(&path(&[3333, 2121]), &from(role)), Ok(Some(2121)) ); } assert_eq!( policy.check_path(&marked_by(2121), &EXTERNAL), Ok(Some(2121)) ); // Not even a policy that takes everything else takes a leak. assert_eq!( ImportPolicy::permissive().check_path(&marked_by(2121), &from(PeerRole::Customer)), Err(Rejection::Leak) ); } #[test] fn roles_pair_up_and_decide_where_the_mark_is_put() { use PeerRole::*; for role in [Provider, Customer, Peer, RouteServer, RouteServerClient] { // What we are to the peer is what the peer is to us, seen // from the other side. let mirrored = [Provider, Customer, Peer, RouteServer, RouteServerClient] .into_iter() .find(|other| other.theirs() == role.ours()) .unwrap(); assert_eq!(mirrored.ours(), role.theirs(), "{role:?}"); } assert_eq!(Provider.ours(), BgpRole::Customer); assert_eq!(RouteServer.ours(), BgpRole::RouteServerClient); assert_eq!(Peer.ours(), BgpRole::Peer); // Marked on the way in from above or beside; on the way out // downwards or across. assert_eq!(Provider.only_to_customer_on_import(3333), Some(3333)); assert_eq!(Peer.only_to_customer_on_import(3333), Some(3333)); assert_eq!(RouteServer.only_to_customer_on_import(3333), Some(3333)); assert_eq!(Customer.only_to_customer_on_import(3333), None); assert_eq!(RouteServerClient.only_to_customer_on_import(3333), None); assert_eq!(Customer.only_to_customer_on_export(1299), Some(1299)); assert_eq!(Peer.only_to_customer_on_export(1299), Some(1299)); assert_eq!( RouteServerClient.only_to_customer_on_export(1299), Some(1299) ); assert_eq!(Provider.only_to_customer_on_export(1299), None); assert_eq!(RouteServer.only_to_customer_on_export(1299), None); assert!(Customer.may_be_sent_only_to_customer_routes()); assert!(!Peer.may_be_sent_only_to_customer_routes()); } #[test] fn the_path_is_read_with_its_sets() { let mut out = vec![99]; assert!(!effective_path(&path(&[3333, 1299]), &mut out)); assert_eq!(out, [3333, 1299]); let sequence = |path: Vec| AsPathSegment { ordered: true, path, }; let with_set = vec![PathAttribute::AsPath(AsPathAttribute { segments: vec![ sequence(vec![3333]), AsPathSegment { ordered: false, path: vec![64500, 64501], }, ], })]; assert!(effective_path(&with_set, &mut out)); assert_eq!(out, [3333, 64500, 64501]); assert!(!effective_path(&[], &mut out)); assert!(out.is_empty()); } /// The counts of refusals are kept by a reason's place in the list. #[test] fn every_reason_is_at_its_own_place_in_the_list() { for (at, rejection) in Rejection::ALL.iter().enumerate() { assert_eq!(rejection.index(), at, "{rejection:?}"); } let names: HashSet<_> = Rejection::ALL.iter().map(|r| r.as_str()).collect(); assert_eq!(names.len(), Rejection::ALL.len()); } /// RFC 9774: nobody can say where a route with a set in its path /// comes from. #[test] fn a_path_with_a_set_is_refused() { let with_set = vec![PathAttribute::AsPath(AsPathAttribute { segments: vec![ AsPathSegment { ordered: true, path: vec![3333, 6939], }, AsPathSegment { ordered: false, path: vec![2121, 2122], }, ], })]; assert_eq!( ImportPolicy::default().check_path(&with_set, &EXTERNAL), Err(Rejection::AsSet) ); let lenient = ImportPolicy { accept_as_sets: true, ..ImportPolicy::default() }; assert_eq!(lenient.check_path(&with_set, &EXTERNAL), Ok(None)); } /// What we originate is ours: a more specific route for part of it /// from another network would draw our own traffic away. #[test] fn our_own_prefixes_are_not_taken_from_others() { let mut own = PrefixSet::default(); own.allow(prefix("193.0.0.0/21"), 32); let policy = ImportPolicy { own_prefixes: Arc::new(own), ..ImportPolicy::default() }; for ours in ["193.0.0.0/21", "193.0.4.0/22", "193.0.7.0/24"] { assert_eq!( policy.check_prefix(&prefix(ours)), Err(Rejection::OwnPrefix), "{ours}" ); } // What covers it, or is beside it, is someone else's. for theirs in ["193.0.0.0/20", "193.0.8.0/24"] { assert_eq!(policy.check_prefix(&prefix(theirs)), Ok(()), "{theirs}"); } // A customer that has a part of it, and is listed as having it. let mut listed = PrefixSet::default(); listed.allow(prefix("193.0.4.0/24"), 24); let customer = ImportPolicy { prefixes: Some(listed), ..policy }; assert_eq!(customer.check_prefix(&prefix("193.0.4.0/24")), Ok(())); assert_eq!( customer.check_prefix(&prefix("193.0.5.0/24")), Err(Rejection::PrefixNotAllowed) ); } /// RFC 4271 §5.1.3 and more: the next hop is where our traffic goes. #[test] fn the_next_hop_has_to_be_the_peer() { let policy = ImportPolicy::default(); let v4 = |s: &str| NlriNextHop::Ipv4(s.parse().unwrap()); let check = |policy: &ImportPolicy, next_hop: &NlriNextHop, from: &RouteSource| { policy.check_next_hop(next_hop, IpVersion::Ipv4, from) }; assert_eq!(check(&policy, &v4("192.0.2.2"), &EXTERNAL), Ok(())); // Another machine on the same network, or anywhere. assert_eq!( check(&policy, &v4("192.0.2.3"), &EXTERNAL), Err(Rejection::NextHopNotPeer) ); // Nowhere at all, whatever the policy. let lenient = ImportPolicy::permissive(); for nowhere in [ "0.0.0.0", "127.0.0.1", "224.0.0.5", "255.255.255.255", "192.0.2.1", ] { for policy in [&policy, &lenient] { assert_eq!( check(policy, &v4(nowhere), &EXTERNAL), Err(Rejection::NextHopUnusable), "{nowhere}" ); } } // Where it may be someone else: said so, a route server, our own AS. assert_eq!(check(&lenient, &v4("192.0.2.3"), &EXTERNAL), Ok(())); for from in [ RouteSource { role: Some(PeerRole::RouteServer), ..EXTERNAL }, RouteSource { ibgp: true, ..EXTERNAL }, ] { assert_eq!(check(&policy, &v4("192.0.2.3"), &from), Ok(())); } // A provider is not a route server. let provider = RouteSource { role: Some(PeerRole::Provider), ..EXTERNAL }; assert_eq!( check(&policy, &v4("192.0.2.3"), &provider), Err(Rejection::NextHopNotPeer) ); } #[test] fn an_ipv6_next_hop_is_judged_in_each_of_its_forms() { let policy = ImportPolicy::default(); let peer: Ipv6Addr = "2001:db8::2".parse().unwrap(); let peer_ll: Ipv6Addr = "fe80::2".parse().unwrap(); let over_v6 = RouteSource { peer_address: peer.into(), local_address: "2001:db8::1".parse().unwrap(), ..EXTERNAL }; let v6 = |policy: &ImportPolicy, next_hop: NlriNextHop, from: &RouteSource| { policy.check_next_hop(&next_hop, IpVersion::Ipv6, from) }; assert_eq!(v6(&policy, NlriNextHop::Ipv6(peer), &over_v6), Ok(())); let both = |global: Ipv6Addr, link_local: Ipv6Addr| NlriNextHop::Ipv6WithLl { global, link_local }; assert_eq!(v6(&policy, both(peer, peer_ll), &over_v6), Ok(())); assert_eq!( v6( &policy, both("2001:db8::3".parse().unwrap(), peer_ll), &over_v6 ), Err(Rejection::NextHopNotPeer) ); // What goes with a global address has to be a link-local one. assert_eq!( v6( &policy, both(peer, "2001:db8::9".parse().unwrap()), &over_v6 ), Err(Rejection::NextHopUnusable) ); assert_eq!( v6(&policy, both(peer, "::1".parse().unwrap()), &over_v6), Err(Rejection::NextHopUnusable) ); // A session between link-local addresses: the peer's own, alone, // twice, or after nothing. let link_local = RouteSource { peer_address: peer_ll.into(), local_address: "fe80::1".parse().unwrap(), ..EXTERNAL }; for next_hop in [ NlriNextHop::Ipv6(peer_ll), both(peer_ll, peer_ll), both(Ipv6Addr::UNSPECIFIED, peer_ll), ] { assert_eq!(v6(&policy, next_hop, &link_local), Ok(()), "{next_hop:?}"); } assert_eq!( v6( &policy, both(Ipv6Addr::UNSPECIFIED, "fe80::3".parse().unwrap()), &link_local ), Err(Rejection::NextHopNotPeer) ); assert_eq!( v6( &policy, NlriNextHop::Ipv6(Ipv6Addr::UNSPECIFIED), &link_local ), Err(Rejection::NextHopUnusable) ); assert_eq!( v6( &policy, NlriNextHop::Ipv6("fe80::1".parse().unwrap()), &link_local ), Err(Rejection::NextHopUnusable), "our own address" ); // An IPv4 route with an IPv6 next hop only where that was agreed // on (RFC 8950). let v4_route = |from: &RouteSource| { policy.check_next_hop(&NlriNextHop::Ipv6(peer), IpVersion::Ipv4, from) }; assert_eq!(v4_route(&over_v6), Err(Rejection::NextHopUnusable)); let agreed = RouteSource { extended_next_hop: true, ..over_v6 }; assert_eq!(v4_route(&agreed), Ok(())); // IPv6 routes over an IPv4 session: the session's addresses say // nothing about the next hop. assert_eq!(v6(&policy, NlriNextHop::Ipv6(peer), &EXTERNAL), Ok(())); assert_eq!( policy.check_next_hop( &NlriNextHop::Ipv4("192.0.2.2".parse().unwrap()), IpVersion::Ipv6, &EXTERNAL ), Err(Rejection::NextHopUnusable) ); } #[test] fn an_absurdly_long_path_is_refused() { let policy = ImportPolicy::default(); assert_eq!( policy.check_path(&path(&[3333; 64]), &EXTERNAL), Ok(Some(3333)) ); assert_eq!( policy.check_path(&path(&[3333; 65]), &EXTERNAL), Err(Rejection::PathTooLong) ); } #[test] fn a_peer_may_announce_only_routes_of_the_origins_listed_for_it() { let policy = ImportPolicy { origin_asns: Some([3333, 2121].into()), ..ImportPolicy::default() }; assert_eq!( policy.check_path(&path(&[3333]), &EXTERNAL).map(drop), Ok(()) ); assert_eq!( policy.check_path(&path(&[3333, 2121]), &EXTERNAL).map(drop), Ok(()) ); assert_eq!( policy.check_path(&path(&[3333, 15169]), &EXTERNAL), Err(Rejection::OriginNotAllowed) ); // A path that ends in a set has no one origin. let policy = ImportPolicy { accept_as_sets: true, ..policy }; let ends_in_set = vec![PathAttribute::AsPath(AsPathAttribute { segments: vec![ AsPathSegment { ordered: true, path: vec![3333], }, AsPathSegment { ordered: false, path: vec![2121], }, ], })]; assert_eq!( policy.check_path(&ends_in_set, &EXTERNAL), Err(Rejection::OriginNotAllowed) ); } #[test] fn a_peer_is_held_down_for_as_long_as_was_said() { let held = HoldDowns::default(); let address = PeerAddress::from("192.0.2.1".parse::().unwrap()); let other = PeerAddress::from("192.0.2.2".parse::().unwrap()); // Two failures soon after coming up are let pass; from the third // the peer is kept down, twice as long each time, up to a limit. let flaps = HoldDowns::default(); let soon = Duration::from_secs(5); assert_eq!(flaps.session_ended(address, soon, true), None); assert_eq!(flaps.session_ended(address, soon, true), None); assert_eq!(flaps.session_ended(address, soon, true), Some(FLAP_HOLD)); assert_eq!( flaps.session_ended(address, soon, true), Some(2 * FLAP_HOLD) ); assert_eq!( flaps.remaining(address).map(|(_, why)| why), Some(HeldDown::Flapping) ); assert_eq!(flaps.remaining(other), None, "each peer for itself"); for _ in 0..40 { flaps.session_ended(address, soon, true); } assert_eq!( flaps.session_ended(address, soon, true), Some(FLAP_HOLD_MAX) ); // A session that lasted, or that we ended ourselves, starts the // count over. assert_eq!(flaps.session_ended(address, FLAP_WITHIN, true), None); assert_eq!(flaps.session_ended(address, soon, true), None); assert_eq!(flaps.session_ended(address, soon, false), None); assert_eq!(flaps.session_ended(address, soon, true), None); assert_eq!(held.remaining(address), None); held.hold(address, Duration::from_secs(600), HeldDown::OverLimit); let (remaining, why) = held.remaining(address).expect("held"); assert!(remaining > Duration::from_secs(590), "{remaining:?}"); assert_eq!(why, HeldDown::OverLimit); assert_eq!(held.remaining(other), None); // Not made shorter by a lesser reason. held.hold(address, Duration::from_secs(1), HeldDown::Flapping); let (remaining, why) = held.remaining(address).expect("held"); assert!(remaining > Duration::from_secs(590), "{remaining:?}"); assert_eq!(why, HeldDown::OverLimit); held.hold(other, Duration::ZERO, HeldDown::OverLimit); assert_eq!(held.remaining(other), None); assert_eq!( held.held.locked().len(), 1, "a hold that is over is forgotten" ); } /// The quick look at a prefix's first octets never lets through what /// the list has. fn assert_quick_check_agrees(prefix: IpPrefix) { assert_eq!( is_bogon_prefix(&prefix), is_listed_bogon(&prefix), "{prefix}" ); } #[test] fn the_quick_bogon_check_agrees_with_the_list_around_every_entry() { for bogon in BOGONS.iter().chain([&*GLOBAL_UNICAST_V6]) { let version = bogon.address_family(); let mut address = [0u8; 16]; address[..bogon.bytes().len()].copy_from_slice(bogon.bytes()); // The entry, what covers it, and what is inside it, at every // length; and the same just past its end. for flip in [None, Some(bogon.length().saturating_sub(1))] { let mut address = address; if let Some(bit) = flip { address[usize::from(bit / 8)] ^= 0x80 >> (bit % 8); } for length in 0..=version.bits() { assert_quick_check_agrees(IpPrefix::new(version, address, length).unwrap()); } } } } proptest! { #[test] fn the_quick_bogon_check_agrees_with_the_list( v6: bool, bytes: [u8; 16], length in 0u8..=128, ) { let version = if v6 { IpVersion::Ipv6 } else { IpVersion::Ipv4 }; let prefix = IpPrefix::new(version, bytes, length.min(version.bits())).unwrap(); assert_quick_check_agrees(prefix); } /// The prefix set agrees with looking at every entry in turn. #[test] fn a_prefix_set_allows_what_some_entry_covers( entries in proptest::collection::vec((any::<[u8; 4]>(), 0u8..=32, 0u8..=32), 0..12), probes in proptest::collection::vec((any::<[u8; 4]>(), 0u8..=32), 1..40), pick in any::(), ) { let entries: Vec<(IpPrefix, u8)> = entries .into_iter() .map(|(bytes, length, extra)| { let prefix = IpPrefix::new(IpVersion::Ipv4, bytes, length).unwrap(); (prefix, length.max(extra)) }) .collect(); let mut set = PrefixSet::default(); for (entry, max_length) in &entries { set.allow(*entry, *max_length); } // Random prefixes rarely fall inside a random entry, so some // probes are made from the entries themselves. let mut probes: Vec = probes .into_iter() .map(|(bytes, length)| IpPrefix::new(IpVersion::Ipv4, bytes, length).unwrap()) .collect(); if !entries.is_empty() { let (entry, _) = entries[pick.index(entries.len())]; for length in entry.length()..=32 { let mut bytes = [0u8; 4]; bytes[..entry.bytes().len()].copy_from_slice(entry.bytes()); probes.push(IpPrefix::new(IpVersion::Ipv4, bytes, length).unwrap()); } } for probe in probes { let expected = entries.iter().any(|(entry, max_length)| { entry.contains(&probe) && probe.length() <= *max_length }); prop_assert_eq!(set.allows(&probe), expected, "{}", probe); } } } }