Files
ferrix/crates/server/src/policy.rs
T
Rayhaan JaufeerallyandClaude Opus 5.5 efb2d81a50
ci / check (push) Canceled after 0s
ci / docs (push) Canceled after 0s
ci / interop (push) Canceled after 0s
ci / fuzz (push) Canceled after 0s
Reject blackhole requests with a reason of their own
A route with the BLACKHOLE community (RFC 7999) asks for the traffic to
its prefix to be dropped. Exchange route servers pass these on as host
routes with the exchange's blackhole address as next hop, and they were
rejected as "prefix too specific", which hid what they are.

Routes from EBGP neighbors with BLACKHOLE are now rejected as "blackhole
request", whatever their length. One short enough to pass would have
been taken as an ordinary route, sending the traffic to the blackhole
next hop: the request honoured without anyone having chosen to. Acting
on blackhole requests is not supported; the import policy page and the
standards list say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 01:03:16 +02:00

1665 lines
61 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, Mutex};
use std::time::{Duration, Instant};
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::address::PeerAddress;
use crate::marks::{self, Marks};
use crate::sync::IgnorePoison;
/// The longest prefixes routed between networks: anything more specific is
/// filtered nearly everywhere, and is usually a leak of internal routes.
const LONGEST_IPV4: u8 = 24;
const LONGEST_IPV6: u8 = 48;
/// The shortest prefixes anyone announces, short of a default route.
const SHORTEST_IPV4: u8 = 8;
const SHORTEST_IPV6: u8 = 16;
/// A path longer than this is not one traffic would be sent along; it is a
/// mistake or an attempt to exhaust something.
const DEFAULT_MAX_AS_PATH_LENGTH: u16 = 64;
/// How long a peer that went over its limit is kept down, unless
/// configured otherwise.
const DEFAULT_HOLD_DOWN: Duration = Duration::from_secs(600);
/// What is taken from one peer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImportPolicy {
/// The most routes the peer may have at a time; `None` for no limit.
pub max_prefixes: Option<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>,
/// 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>,
}
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(),
marks: Marks::default(),
local_pref: 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 are changed on their way in.
pub fn changes_routes(&self) -> bool {
!self.marks.is_empty() || self.local_pref.is_some()
}
/// Makes of `attributes` what the route is kept with: marked, and
/// with its preference.
pub fn change(&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)),
);
}
}
}
/// 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,
}
/// 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,
}
impl Rejection {
/// Whether the reason may pass by itself, with the route unchanged:
/// the RPKI's data changes, and so do the machine's interfaces. A
/// route refused for such a reason is held, to be taken when it does.
pub(crate) fn may_pass(self) -> bool {
matches!(
self,
Self::RpkiInvalid | Self::AspaInvalid | Self::NextHopUnreachable
)
}
pub const ALL: [Self; 18] = [
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,
];
/// 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",
}
}
}
/// 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 {
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 list of prefixes a peer may announce. Each entry allows the prefix
/// itself and, if it says so, the more specific prefixes inside it up to a
/// length.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PrefixSet {
/// For each prefix listed, the longest prefix inside it that is
/// allowed.
entries: HashMap<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 len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
/// Whether `prefix` is allowed by some entry.
pub fn allows(&self, prefix: &IpPrefix) -> bool {
// An entry that covers it is the prefix cut to the entry's length,
// so there is one place to look for each length there is.
self.lengths[Self::version_index(prefix)]
.iter()
.take_while(|length| **length <= prefix.length())
.any(|length| {
self.entries
.get(&prefix.shortened(*length))
.is_some_and(|longest| prefix.length() <= *longest)
})
}
fn version_index(prefix: &IpPrefix) -> usize {
match prefix.address_family() {
IpVersion::Ipv4 => 0,
IpVersion::Ipv6 => 1,
}
}
}
/// Why a peer is being kept down.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HeldDown {
/// It went over its limit of routes.
OverLimit,
/// Its sessions keep coming up and failing.
Flapping,
}
impl HeldDown {
pub fn as_str(self) -> &'static str {
match self {
Self::OverLimit => "held down after going over its limit of routes",
Self::Flapping => "held down because its sessions keep failing",
}
}
}
/// A session that fails sooner than this after coming up counts as a
/// flap.
const FLAP_WITHIN: Duration = Duration::from_secs(60);
/// This many flaps in a row are let pass: a router being restarted, a
/// cable put back.
const FLAPS_ALLOWED: u32 = 2;
/// How long a peer is kept down after the first flap too many; twice as
/// long after each one more, up to `FLAP_HOLD_MAX`.
const FLAP_HOLD: Duration = Duration::from_secs(1);
const FLAP_HOLD_MAX: Duration = Duration::from_secs(300);
/// Peers that are being kept down, and until when: those that went over
/// their limit, and those whose sessions keep failing. Shared by the
/// sessions, which put peers here, and by the dialer and the listeners,
/// which leave them alone meanwhile.
///
/// Every session that comes up and goes costs a look at every route in
/// the table, and tells every other peer of the changes. A peer that
/// does so over and over, by fault or by design, is slowed down.
#[derive(Debug, Default)]
pub struct HoldDowns {
held: Mutex<HashMap<PeerAddress, (Instant, HeldDown)>>,
/// How many times in a row each peer's session has failed soon after
/// coming up.
flaps: Mutex<HashMap<PeerAddress, u32>>,
}
impl HoldDowns {
/// Keeps the peer at `address` down for `duration` from now.
pub fn hold(&self, address: PeerAddress, duration: Duration, why: HeldDown) {
let until = Instant::now() + duration;
let mut held = self.held.locked();
// Not for less long than it already is.
if held
.get(&address)
.is_none_or(|(already, _)| *already < until)
{
held.insert(address, (until, why));
}
}
/// How much longer the peer at `address` is kept down, if it is, and
/// why.
pub fn remaining(&self, address: PeerAddress) -> Option<(Duration, HeldDown)> {
let mut held = self.held.locked();
let (until, why) = *held.get(&address)?;
let remaining = until.saturating_duration_since(Instant::now());
if remaining.is_zero() {
held.remove(&address);
return None;
}
Some((remaining, why))
}
/// A session with the peer at `address` that had come up has ended,
/// `lasted` after it did; `failed` if not at our own wish. Returns for
/// how long the peer is kept down for it, if it is.
pub fn session_ended(
&self,
address: PeerAddress,
lasted: Duration,
failed: bool,
) -> Option<Duration> {
let mut flaps = self.flaps.locked();
if !failed || lasted >= FLAP_WITHIN {
flaps.remove(&address);
return None;
}
let count = flaps.entry(address).or_insert(0);
*count += 1;
let over = count.checked_sub(FLAPS_ALLOWED + 1)?;
let hold = FLAP_HOLD
.checked_mul(1u32.checked_shl(over).unwrap_or(u32::MAX))
.map_or(FLAP_HOLD_MAX, |hold| hold.min(FLAP_HOLD_MAX));
drop(flaps);
self.hold(address, hold, HeldDown::Flapping);
Some(hold)
}
}
#[cfg(test)]
mod tests {
use bgp_packet::message::{AsPathSegment, 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(())
);
}
#[test]
fn the_path_must_begin_with_the_peers_as_unless_it_is_a_route_server() {
let policy = ImportPolicy::default();
assert_eq!(
policy.check_path(&path(&[3333, 2914]), &EXTERNAL).map(drop),
Ok(())
);
assert_eq!(
policy.check_path(&path(&[2914, 3333]), &EXTERNAL),
Err(Rejection::FirstAs)
);
assert_eq!(
policy.check_path(&path(&[]), &EXTERNAL),
Err(Rejection::FirstAs)
);
// A set has no first AS.
let set = vec![PathAttribute::AsPath(AsPathAttribute {
segments: vec![AsPathSegment {
ordered: false,
path: vec![3333],
}],
})];
assert_eq!(policy.check_path(&set, &EXTERNAL), Err(Rejection::FirstAs));
let route_server = ImportPolicy {
accept_any_first_as: true,
..ImportPolicy::default()
};
assert_eq!(
route_server
.check_path(&path(&[2914, 174]), &EXTERNAL)
.map(drop),
Ok(())
);
}
#[test]
fn reserved_and_private_as_numbers_in_the_path_are_refused() {
let policy = ImportPolicy::default();
for bogon in [
0,
23456,
64496,
64512,
65000,
65535,
65536,
131071,
4_200_000_000,
u32::MAX,
] {
assert_eq!(
policy.check_path(&path(&[3333, bogon]), &EXTERNAL),
Err(Rejection::BogonAs),
"AS{bogon}"
);
}
for real in [174, 64495, 131072, 210036, 4_199_999_999] {
assert_eq!(
policy.check_path(&path(&[3333, real]), &EXTERNAL).map(drop),
Ok(()),
"AS{real}"
);
}
}
/// RFC 9234 §5, the ingress procedure.
#[test]
fn a_route_only_for_customers_is_a_leak_from_below_or_from_a_second_peer() {
let policy = ImportPolicy::default();
let marked_by = |asn: u32| {
let mut attributes = path(&[3333, 2121]);
attributes.push(PathAttribute::OnlyToCustomer(asn));
attributes
};
let from = |role| RouteSource {
role: Some(role),
..EXTERNAL
};
// From below it is always a leak.
for role in [PeerRole::Customer, PeerRole::RouteServerClient] {
for asn in [3333, 2121] {
assert_eq!(
policy.check_path(&marked_by(asn), &from(role)),
Err(Rejection::Leak),
"{role:?}"
);
}
}
// From a peer, unless the peer marked it itself.
assert_eq!(
policy.check_path(&marked_by(2121), &from(PeerRole::Peer)),
Err(Rejection::Leak)
);
assert_eq!(
policy.check_path(&marked_by(3333), &from(PeerRole::Peer)),
Ok(Some(2121))
);
// From above it is where it should be.
for role in [PeerRole::Provider, PeerRole::RouteServer] {
assert_eq!(
policy.check_path(&marked_by(2121), &from(role)),
Ok(Some(2121)),
"{role:?}"
);
}
// Without the mark nothing is known, whoever it is from; and
// without knowing what the peer is, the mark says nothing.
for role in [PeerRole::Customer, PeerRole::Peer, PeerRole::Provider] {
assert_eq!(
policy.check_path(&path(&[3333, 2121]), &from(role)),
Ok(Some(2121))
);
}
assert_eq!(
policy.check_path(&marked_by(2121), &EXTERNAL),
Ok(Some(2121))
);
// Not even a policy that takes everything else takes a leak.
assert_eq!(
ImportPolicy::permissive().check_path(&marked_by(2121), &from(PeerRole::Customer)),
Err(Rejection::Leak)
);
}
#[test]
fn roles_pair_up_and_decide_where_the_mark_is_put() {
use PeerRole::*;
for role in [Provider, Customer, Peer, RouteServer, RouteServerClient] {
// What we are to the peer is what the peer is to us, seen
// from the other side.
let mirrored = [Provider, Customer, Peer, RouteServer, RouteServerClient]
.into_iter()
.find(|other| other.theirs() == role.ours())
.unwrap();
assert_eq!(mirrored.ours(), role.theirs(), "{role:?}");
}
assert_eq!(Provider.ours(), BgpRole::Customer);
assert_eq!(RouteServer.ours(), BgpRole::RouteServerClient);
assert_eq!(Peer.ours(), BgpRole::Peer);
// Marked on the way in from above or beside; on the way out
// downwards or across.
assert_eq!(Provider.only_to_customer_on_import(3333), Some(3333));
assert_eq!(Peer.only_to_customer_on_import(3333), Some(3333));
assert_eq!(RouteServer.only_to_customer_on_import(3333), Some(3333));
assert_eq!(Customer.only_to_customer_on_import(3333), None);
assert_eq!(RouteServerClient.only_to_customer_on_import(3333), None);
assert_eq!(Customer.only_to_customer_on_export(1299), Some(1299));
assert_eq!(Peer.only_to_customer_on_export(1299), Some(1299));
assert_eq!(
RouteServerClient.only_to_customer_on_export(1299),
Some(1299)
);
assert_eq!(Provider.only_to_customer_on_export(1299), None);
assert_eq!(RouteServer.only_to_customer_on_export(1299), None);
assert!(Customer.may_be_sent_only_to_customer_routes());
assert!(!Peer.may_be_sent_only_to_customer_routes());
}
#[test]
fn the_path_is_read_with_its_sets() {
let mut out = vec![99];
assert!(!effective_path(&path(&[3333, 1299]), &mut out));
assert_eq!(out, [3333, 1299]);
let sequence = |path: Vec<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)
);
}
#[test]
fn a_peer_is_held_down_for_as_long_as_was_said() {
let held = HoldDowns::default();
let address = PeerAddress::from("192.0.2.1".parse::<std::net::IpAddr>().unwrap());
let other = PeerAddress::from("192.0.2.2".parse::<std::net::IpAddr>().unwrap());
// Two failures soon after coming up are let pass; from the third
// the peer is kept down, twice as long each time, up to a limit.
let flaps = HoldDowns::default();
let soon = Duration::from_secs(5);
assert_eq!(flaps.session_ended(address, soon, true), None);
assert_eq!(flaps.session_ended(address, soon, true), None);
assert_eq!(flaps.session_ended(address, soon, true), Some(FLAP_HOLD));
assert_eq!(
flaps.session_ended(address, soon, true),
Some(2 * FLAP_HOLD)
);
assert_eq!(
flaps.remaining(address).map(|(_, why)| why),
Some(HeldDown::Flapping)
);
assert_eq!(flaps.remaining(other), None, "each peer for itself");
for _ in 0..40 {
flaps.session_ended(address, soon, true);
}
assert_eq!(
flaps.session_ended(address, soon, true),
Some(FLAP_HOLD_MAX)
);
// A session that lasted, or that we ended ourselves, starts the
// count over.
assert_eq!(flaps.session_ended(address, FLAP_WITHIN, true), None);
assert_eq!(flaps.session_ended(address, soon, true), None);
assert_eq!(flaps.session_ended(address, soon, false), None);
assert_eq!(flaps.session_ended(address, soon, true), None);
assert_eq!(held.remaining(address), None);
held.hold(address, Duration::from_secs(600), HeldDown::OverLimit);
let (remaining, why) = held.remaining(address).expect("held");
assert!(remaining > Duration::from_secs(590), "{remaining:?}");
assert_eq!(why, HeldDown::OverLimit);
assert_eq!(held.remaining(other), None);
// Not made shorter by a lesser reason.
held.hold(address, Duration::from_secs(1), HeldDown::Flapping);
let (remaining, why) = held.remaining(address).expect("held");
assert!(remaining > Duration::from_secs(590), "{remaining:?}");
assert_eq!(why, HeldDown::OverLimit);
held.hold(other, Duration::ZERO, HeldDown::OverLimit);
assert_eq!(held.remaining(other), None);
assert_eq!(
held.held.locked().len(),
1,
"a hold that is over is forgotten"
);
}
/// The quick look at a prefix's first octets never lets through what
/// the list has.
fn assert_quick_check_agrees(prefix: IpPrefix) {
assert_eq!(
is_bogon_prefix(&prefix),
is_listed_bogon(&prefix),
"{prefix}"
);
}
#[test]
fn the_quick_bogon_check_agrees_with_the_list_around_every_entry() {
for bogon in BOGONS.iter().chain([&*GLOBAL_UNICAST_V6]) {
let version = bogon.address_family();
let mut address = [0u8; 16];
address[..bogon.bytes().len()].copy_from_slice(bogon.bytes());
// The entry, what covers it, and what is inside it, at every
// length; and the same just past its end.
for flip in [None, Some(bogon.length().saturating_sub(1))] {
let mut address = address;
if let Some(bit) = flip {
address[usize::from(bit / 8)] ^= 0x80 >> (bit % 8);
}
for length in 0..=version.bits() {
assert_quick_check_agrees(IpPrefix::new(version, address, length).unwrap());
}
}
}
}
proptest! {
#[test]
fn the_quick_bogon_check_agrees_with_the_list(
v6: bool, bytes: [u8; 16], length in 0u8..=128,
) {
let version = if v6 { IpVersion::Ipv6 } else { IpVersion::Ipv4 };
let prefix = IpPrefix::new(version, bytes, length.min(version.bits())).unwrap();
assert_quick_check_agrees(prefix);
}
/// The prefix set agrees with looking at every entry in turn.
#[test]
fn a_prefix_set_allows_what_some_entry_covers(
entries in proptest::collection::vec((any::<[u8; 4]>(), 0u8..=32, 0u8..=32), 0..12),
probes in proptest::collection::vec((any::<[u8; 4]>(), 0u8..=32), 1..40),
pick in any::<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);
}
}
}
}