Five functions did several things each in one body of 120 to 190 lines. Each is now a list of steps with a name and a doc comment of their own, with nothing changed in what they do: - run_session: hold_down_after, keep_for and release_routes. - Session::establish: session_handle, validation and route_source. A session keeps the peer's settings as one PeerSettings rather than eight fields copied out of it one by one; the sweeps' two channel ends are named fields, not .0 and .1; whether the peer can be asked to send its routes again is a bool, not a copy of the families. - Fsm::on_open: negotiate_role and negotiate_add_path. - BgpServer::run: log_startup and spawn_tasks. - Config::from_proto: parse_listen_endpoints, parse_peers, check_roles, parse_originate and Ours::tell_peers, with the three per-peer defaults in one PeerDefaults. - Egress::outgoing: may_go, rewrite and next_hop_for. - Exporter::follow: Learned::drain, judge and flush, with the three things called "announced" told apart (own_announced, learned.announced, batches) and the verdicts given a type. The hold-downs are a module of their own, holddown.rs; policy.rs is about policy again. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1723 lines
64 KiB
Rust
1723 lines
64 KiB
Rust
//! 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};
|
|
use std::time::Duration;
|
|
|
|
use bgp_packet::message::{
|
|
AS_TRANS, AsPathAttribute, LocalPrefPathAttribute, NlriNextHop, PathAttribute,
|
|
};
|
|
use bgp_packet::open::BgpRole;
|
|
use bgp_primitives::constants::IpVersion;
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
|
|
use crate::marks::{self, Marks, Patterns, Scrub};
|
|
|
|
/// 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<u32>,
|
|
/// 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<PrefixSet>,
|
|
/// The AS numbers the peer's routes may originate from; `None` for
|
|
/// any.
|
|
pub origin_asns: Option<HashSet<u32>>,
|
|
/// 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<PrefixSet>,
|
|
/// The prefixes this router originates, each by itself. A peer in our
|
|
/// own AS that announces one of them originates it too, as with an
|
|
/// anycast prefix that both announce: its route is of no use here,
|
|
/// where the prefix is local, and is ignored, neither taken nor
|
|
/// counted as rejected. Empty for a peer in another AS, whose routes
|
|
/// for them `own_prefixes` refuses.
|
|
pub originated: Arc<PrefixSet>,
|
|
/// Keep the communities of our own AS on the peer's routes, for it to
|
|
/// steer them with control communities: a customer. Without it, those
|
|
/// of a peer in another AS are taken off by `scrub`.
|
|
pub accept_control_communities: bool,
|
|
/// The communities of our own AS taken off the peer's routes as they
|
|
/// come in, first; `None` for a peer in our own AS and for one with
|
|
/// `accept_control_communities`.
|
|
pub scrub: Option<Scrub>,
|
|
/// Communities taken off and put on every route that is taken.
|
|
pub marks: Marks,
|
|
/// The LOCAL_PREF every route that is taken is given; `None` leaves
|
|
/// it at the default.
|
|
pub local_pref: Option<u32>,
|
|
/// What is done to some of the routes that are taken, beyond the
|
|
/// above: each route by the first rule that holds for it.
|
|
pub rules: Vec<ImportRule>,
|
|
/// For a peer in our own AS, what a route passed on inside the AS
|
|
/// must not say it has already been through (RFC 4456 §8); `None` for
|
|
/// a peer in another AS, whose routes carry no such thing.
|
|
pub reflection: Option<Reflection>,
|
|
}
|
|
|
|
/// Who we are to routes passed on from one router of our AS to another:
|
|
/// a route whose ORIGINATOR_ID is our BGP Identifier started here, and one
|
|
/// whose CLUSTER_LIST holds our cluster ID has been passed on by us
|
|
/// already. Either has gone round in a circle, and is not taken.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct Reflection {
|
|
pub router_id: Ipv4Addr,
|
|
pub cluster_id: Ipv4Addr,
|
|
}
|
|
|
|
/// What is done to the routes with some communities, or for some
|
|
/// prefixes, as they are taken.
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
pub struct ImportRule {
|
|
/// The rule is for routes with one of these communities, as the
|
|
/// route has them once the import policy has marked it; for any if
|
|
/// empty.
|
|
pub communities: Patterns,
|
|
/// And for routes whose prefix is in these; for any if `None`.
|
|
pub prefixes: Option<PrefixSet>,
|
|
/// The LOCAL_PREF it gives them, in place of the policy's.
|
|
pub local_pref: Option<u32>,
|
|
/// Communities it puts on them.
|
|
pub marks: Marks,
|
|
}
|
|
|
|
impl ImportRule {
|
|
/// Whether the rule is for routes with `attributes`, as far as their
|
|
/// communities go: the same for all of an UPDATE's routes.
|
|
pub fn holds_for(&self, attributes: &[PathAttribute]) -> bool {
|
|
self.communities.is_empty() || self.communities.match_any(attributes)
|
|
}
|
|
|
|
/// Whether it is for routes for `prefix`, as far as that goes.
|
|
pub fn holds_for_prefix(&self, prefix: &IpPrefix) -> bool {
|
|
self.prefixes
|
|
.as_ref()
|
|
.is_none_or(|prefixes| prefixes.allows(prefix))
|
|
}
|
|
|
|
pub fn apply(&self, attributes: &mut Vec<PathAttribute>) {
|
|
self.marks.apply(attributes);
|
|
if let Some(local_pref) = self.local_pref {
|
|
marks::put(
|
|
attributes,
|
|
PathAttribute::LocalPref(LocalPrefPathAttribute(local_pref)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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(),
|
|
originated: Arc::default(),
|
|
accept_control_communities: false,
|
|
scrub: None,
|
|
marks: Marks::default(),
|
|
local_pref: None,
|
|
rules: Vec::new(),
|
|
reflection: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ImportPolicy {
|
|
/// Whether routes with `attributes` are changed on their way in.
|
|
/// Asked of every UPDATE: those that come as they are to be kept are
|
|
/// taken as they came, without being written out again.
|
|
pub fn changes(&self, attributes: &[PathAttribute]) -> bool {
|
|
!self.marks.is_empty()
|
|
|| self.local_pref.is_some()
|
|
|| self.scrub.is_some_and(|scrub| scrub.finds(attributes))
|
|
}
|
|
|
|
/// Makes of `attributes` what the route is kept with: without what
|
|
/// others may not say for us, marked, and with its preference.
|
|
pub fn change(&self, attributes: &mut Vec<PathAttribute>) {
|
|
if let Some(scrub) = self.scrub {
|
|
scrub.apply(attributes);
|
|
}
|
|
self.marks.apply(attributes);
|
|
if let Some(local_pref) = self.local_pref {
|
|
marks::put(
|
|
attributes,
|
|
PathAttribute::LocalPref(LocalPrefPathAttribute(local_pref)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// What is announced to one peer, and how.
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
pub struct ExportPolicy {
|
|
/// Pass on the best routes learned from other peers, and not only
|
|
/// what we originate.
|
|
pub learned_routes: bool,
|
|
/// Communities taken off and put on every route announced.
|
|
pub marks: Marks,
|
|
/// How many more times our AS goes on the path.
|
|
pub prepend: u8,
|
|
}
|
|
|
|
/// Limits on what is announced to a network that is not our customer,
|
|
/// which hold whatever the export policy and the routes say: a check on
|
|
/// both, in case either goes wrong.
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
pub struct ExportLimits {
|
|
/// The prefixes that may be announced; `None` for any.
|
|
pub prefixes: Option<PrefixSet>,
|
|
/// The most routes announced at a time.
|
|
pub max_prefixes: Option<u32>,
|
|
}
|
|
|
|
impl ExportLimits {
|
|
/// Whether they are for a session to a peer that is `role` to us, in
|
|
/// another AS or not (`ibgp`): one above or beside us, or one we know
|
|
/// nothing of. Customers, route server clients and our own routers
|
|
/// are told of whatever they are to be.
|
|
pub fn apply_to(role: Option<PeerRole>, ibgp: bool) -> bool {
|
|
!ibgp && role.is_none_or(PeerRole::is_above_or_beside)
|
|
}
|
|
|
|
/// Whether a route for `prefix` may be announced.
|
|
pub fn allows(&self, prefix: &IpPrefix) -> bool {
|
|
self.prefixes
|
|
.as_ref()
|
|
.is_none_or(|prefixes| prefixes.allows(prefix))
|
|
}
|
|
|
|
/// The limit that `routes` routes at a time would go over, if they
|
|
/// would.
|
|
pub fn exceeded_by(&self, routes: usize) -> Option<u32> {
|
|
self.max_prefixes.filter(|limit| routes > *limit as usize)
|
|
}
|
|
}
|
|
|
|
/// 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 route asks for the traffic to its prefix to be dropped: it has
|
|
/// the BLACKHOLE community (RFC 7999). Such requests are not acted on,
|
|
/// and the route is not taken as an ordinary one either, which would
|
|
/// send the traffic to wherever its next hop drops it.
|
|
Blackhole,
|
|
/// 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 prefix is a network of one of this machine's interfaces, or
|
|
/// inside one: the machine reaches it directly, and a route by way of
|
|
/// a peer would compete with that.
|
|
OwnNetwork,
|
|
/// The route was passed on inside our AS and has come back: it says it
|
|
/// started at this router, or was passed on by it (RFC 4456 §8).
|
|
ReflectionLoop,
|
|
}
|
|
|
|
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 | Self::OwnNetwork
|
|
)
|
|
}
|
|
|
|
pub const ALL: [Self; 20] = [
|
|
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::Blackhole,
|
|
Self::NextHopUnusable,
|
|
Self::NextHopNotPeer,
|
|
Self::OwnPrefix,
|
|
Self::NextHopUnreachable,
|
|
Self::OwnNetwork,
|
|
Self::ReflectionLoop,
|
|
];
|
|
|
|
/// 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::Blackhole => "blackhole request",
|
|
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::OwnNetwork => "prefix is a network of this router",
|
|
Self::ReflectionLoop => "route reflected back to us",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The community that asks for the traffic to a prefix to be dropped
|
|
/// (RFC 7999).
|
|
const BLACKHOLE: (u16, u16) = (0xFFFF, 0x029A);
|
|
|
|
/// 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<u32> {
|
|
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<u32> {
|
|
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",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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<PeerRole>,
|
|
/// 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<Option<u32>, 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 {
|
|
if let Some(us) = self.reflection {
|
|
let came_back = attributes.iter().any(|attribute| match attribute {
|
|
PathAttribute::OriginatorId(originator) => *originator == us.router_id,
|
|
PathAttribute::ClusterList(clusters) => clusters.contains(&us.cluster_id),
|
|
_ => false,
|
|
});
|
|
if came_back {
|
|
return Err(Rejection::ReflectionLoop);
|
|
}
|
|
}
|
|
return Ok(origin);
|
|
}
|
|
|
|
if facts.has_local_asn {
|
|
return Err(Rejection::OwnAs);
|
|
}
|
|
let blackhole = attributes.iter().any(|attribute| {
|
|
matches!(attribute, PathAttribute::Communities(communities)
|
|
if communities.0.contains(&BLACKHOLE))
|
|
});
|
|
if blackhole {
|
|
return Err(Rejection::Blackhole);
|
|
}
|
|
// 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<Ipv6Addr>) = 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<u32>,
|
|
}
|
|
|
|
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<u32>) -> 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<Item = u32> + '_ {
|
|
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<Vec<IpPrefix>> = 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<IpPrefix> =
|
|
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 set of prefixes: what a peer may announce, what is our own, what may
|
|
/// be announced to a peer. 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<IpPrefix, u8>,
|
|
/// The lengths of the prefixes listed, by IP version: the only lengths
|
|
/// at which a covering entry can be.
|
|
lengths: [Vec<u8>; 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 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,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use bgp_packet::message::{AsPathSegment, CommunitiesPathAttribute};
|
|
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<PathAttribute> {
|
|
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(())
|
|
);
|
|
}
|
|
|
|
/// A route passed on inside our AS that says it started here, or that
|
|
/// it was passed on by our cluster already, has gone round in a circle
|
|
/// (RFC 4456 §8).
|
|
#[test]
|
|
fn a_reflected_route_that_comes_back_is_not_taken() {
|
|
let us = Ipv4Addr::new(10, 0, 0, 1);
|
|
let cluster = Ipv4Addr::new(10, 0, 0, 9);
|
|
let policy = ImportPolicy {
|
|
reflection: Some(Reflection {
|
|
router_id: us,
|
|
cluster_id: cluster,
|
|
}),
|
|
..ImportPolicy::default()
|
|
};
|
|
let internal = RouteSource {
|
|
peer_asn: 1299,
|
|
ibgp: true,
|
|
..EXTERNAL
|
|
};
|
|
let reflected = |originator, clusters: &[Ipv4Addr]| {
|
|
let mut attributes = path(&[]);
|
|
attributes.push(PathAttribute::OriginatorId(originator));
|
|
attributes.push(PathAttribute::ClusterList(clusters.to_vec()));
|
|
attributes
|
|
};
|
|
let elsewhere = Ipv4Addr::new(10, 0, 0, 5);
|
|
let other_cluster = Ipv4Addr::new(10, 0, 0, 7);
|
|
assert_eq!(
|
|
policy
|
|
.check_path(&reflected(elsewhere, &[other_cluster]), &internal)
|
|
.map(drop),
|
|
Ok(())
|
|
);
|
|
assert_eq!(
|
|
policy.check_path(&reflected(us, &[other_cluster]), &internal),
|
|
Err(Rejection::ReflectionLoop)
|
|
);
|
|
assert_eq!(
|
|
policy.check_path(&reflected(elsewhere, &[other_cluster, cluster]), &internal),
|
|
Err(Rejection::ReflectionLoop)
|
|
);
|
|
}
|
|
|
|
#[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());
|
|
}
|
|
|
|
/// AS_TRANS (23456) is a bogon where real numbers fit, and stands in
|
|
/// for one that does not on a session with two-octet AS numbers.
|
|
#[test]
|
|
fn as_trans_is_a_bogon_only_where_wide_numbers_fit() {
|
|
assert!(is_bogon_asn(u32::from(AS_TRANS), true));
|
|
assert!(!is_bogon_asn(u32::from(AS_TRANS), false));
|
|
assert!(is_bogon_asn(0, false));
|
|
assert!(is_bogon_asn(64512, false));
|
|
assert!(!is_bogon_asn(3333, false));
|
|
}
|
|
|
|
/// Export limits are for networks above or beside us, and for one we
|
|
/// know nothing of; not for customers, route server clients or our own
|
|
/// routers, which are told of what they are to be.
|
|
#[test]
|
|
fn export_limits_are_for_those_above_or_beside_us() {
|
|
use PeerRole::*;
|
|
for role in [Provider, Peer, RouteServer] {
|
|
assert!(ExportLimits::apply_to(Some(role), false), "{role:?}");
|
|
}
|
|
assert!(ExportLimits::apply_to(None, false));
|
|
for role in [Customer, RouteServerClient] {
|
|
assert!(!ExportLimits::apply_to(Some(role), false), "{role:?}");
|
|
}
|
|
assert!(!ExportLimits::apply_to(None, true));
|
|
|
|
let limits = ExportLimits {
|
|
prefixes: None,
|
|
max_prefixes: Some(2),
|
|
};
|
|
assert_eq!(limits.exceeded_by(2), None);
|
|
assert_eq!(limits.exceeded_by(3), Some(2));
|
|
assert!(limits.allows(&IpPrefix::try_from("203.0.113.0/24").unwrap()));
|
|
assert_eq!(ExportLimits::default().exceeded_by(usize::MAX), None);
|
|
}
|
|
|
|
#[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<u32>| 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));
|
|
}
|
|
|
|
/// A request to drop traffic is neither acted on nor taken as an
|
|
/// ordinary route, whose next hop would drop the traffic all the same.
|
|
/// It is told apart from a prefix that is merely too long, which is
|
|
/// what the request usually comes as.
|
|
#[test]
|
|
fn a_blackhole_request_is_refused_as_one() {
|
|
let mut attributes = path(&[3333, 2121]);
|
|
attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(vec![
|
|
(3333, 100),
|
|
(0xFFFF, 0x029A),
|
|
(0xFFFF, 0xFF01),
|
|
])));
|
|
assert_eq!(
|
|
ImportPolicy::default().check_path(&attributes, &EXTERNAL),
|
|
Err(Rejection::Blackhole)
|
|
);
|
|
assert_eq!(Rejection::Blackhole.as_str(), "blackhole request");
|
|
// Other communities are nothing to the policy.
|
|
attributes.pop();
|
|
attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(vec![
|
|
(3333, 666),
|
|
(0xFFFF, 0xFF01),
|
|
])));
|
|
assert_eq!(
|
|
ImportPolicy::default().check_path(&attributes, &EXTERNAL),
|
|
Ok(Some(2121))
|
|
);
|
|
}
|
|
|
|
/// 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)
|
|
);
|
|
}
|
|
|
|
/// 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::<prop::sample::Index>(),
|
|
) {
|
|
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<IpPrefix> = 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);
|
|
}
|
|
}
|
|
}
|
|
}
|