Files
ferrix/crates/server/src/policy.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 0829f873f2 Server: the long functions in named steps
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>
2026-10-06 11:32:20 +02:00

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);
}
}
}
}