Files
ferrix/crates/server/src/routes.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 e2b1979e38 Judge every route a peer announces: import policy, prefix limit, hold-down
A configured peer could announce any prefix with any path, in any
number, and all of it was stored and could be chosen as best. Each
peer now has an import policy, and every route goes through it before
it reaches the routing table.

Checks that hold for any peer unless turned off for it:
- no prefixes that are not routed on the Internet (private, loopback,
  link-local, documentation, multicast, reserved; for IPv6, anything
  outside global unicast);
- nothing longer than /24 or /48, no default route, nothing shorter
  than /8 or /16;
- no reserved, private or documentation AS number in the path;
- a path that begins with the peer's own AS (turned off for a route
  server), and of at most 64 AS numbers.

Always: a route with our own AS number in its path is refused. RFC 4271
requires this loop check and it was missing.

Per peer, if configured: the prefixes it may announce, each with an
optional longest more-specific, and the AS numbers its routes may
originate from. This is the list generated from the routing registries.

max_prefixes limits how many routes a peer may have. One more ends the
session with Cease / Maximum Number of Prefixes Reached, and the peer
is kept down for hold_down_seconds (ten minutes unless set): its
connections are not taken and it is not dialled. Refused routes do not
count towards the limit.

A refused route is withdrawn rather than ignored, so that it replaces
whatever the peer had announced for it before.

On a session with two-octet AS numbers the path is judged with its
AS4_PATH merged in, so that the real AS numbers are what is checked.

Peers that are not configured (accept_unconfigured_peers, for test
networks) get a policy that takes everything but loops.

Refusals are counted in bgp_routes_rejected_total by reason. The status
page and /api/peers show, for each session, how many routes the peer
has and how many were refused. The server warns at startup about peers
with no limit.

The prefix check costs about 4 ns a route with the default policy; the
end-to-end benchmark is unchanged.

Also: the benchmark's server AS is one that is in none of its generated
paths, and a table test that depended on the hasher's seed is fixed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 12:59:37 +02:00

132 lines
4.8 KiB
Rust

//! Putting the routes of a received UPDATE into the routing table, as far
//! as the peer's import policy lets them in.
use std::borrow::Cow;
use std::sync::Arc;
use bgp_packet::message::{NlriNextHop, PathAttribute, UpdateMessage};
use bgp_primitives::ip_prefix::Nlri;
use bgp_primitives::parser::EncodeError;
use bgp_rib::table::{AttrSet, Outcome, PeerIndex, RibTable};
use crate::metrics::ROUTES_REJECTED;
use crate::policy::{From, ImportPolicy, Rejection};
/// What applying an UPDATE did.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct Applied {
/// What changed in the table.
pub(crate) outcome: Outcome,
/// How many routes the UPDATE announced that the policy refused.
pub(crate) rejected: usize,
}
/// Whose UPDATE is being applied, and by what rules.
pub(crate) struct Import<'a> {
pub(crate) peer: PeerIndex,
pub(crate) from: From,
pub(crate) policy: &'a ImportPolicy,
}
/// Applies `update` to `table`: its withdrawals first and then its
/// announcements, so that a prefix named in both ends up announced
/// (RFC 4271 §4.3).
///
/// An UPDATE names prefixes in four places: withdrawn routes and NLRI in the
/// message body, which are IPv4, and the MP_UNREACH_NLRI and MP_REACH_NLRI
/// attributes for every other family (RFC 4760). Where the peer sends
/// several routes for a prefix (ADD-PATH, RFC 7911), each is named by its
/// prefix and path identifier together, and is a route of its own.
///
/// A route the policy refuses is withdrawn instead: whatever the peer had
/// announced for it before is replaced by this, and this is not taken.
///
/// `attributes_wire` is the attributes as the decoder kept them, if it did
/// (see `DecodedUpdate::attributes_wire`). With it the table finds the
/// attribute set from those bytes, without writing the attributes out again.
pub(crate) fn apply(
table: &RibTable,
import: &Import<'_>,
update: &UpdateMessage,
attributes_wire: Option<&[u8]>,
) -> Result<Applied, EncodeError> {
let peer = import.peer;
let mut applied = Applied::default();
if !update.withdrawn_routes.is_empty() {
applied.outcome += table.withdraw(peer, &update.withdrawn_routes);
}
for attribute in &update.path_attributes {
if let PathAttribute::MpUnreachNlri(mp) = attribute {
applied.outcome += table.withdraw(peer, &mp.prefixes);
}
}
// What the routes have in common is judged once.
let path_verdict = import
.policy
.check_path(&update.path_attributes, &import.from);
let attributes = |next_hop: Option<&NlriNextHop>| -> Result<Arc<AttrSet>, EncodeError> {
if let Some(set) =
attributes_wire.and_then(|wire| table.attrs().intern_received(wire, next_hop))
{
return Ok(set);
}
table.attrs().intern(&update.path_attributes, next_hop)
};
let mut announce = |nlri: &[Nlri], next_hop| -> Result<(), EncodeError> {
if nlri.is_empty() {
return Ok(());
}
let mut refuse = |refused: &[Nlri], rejection: Rejection| {
ROUTES_REJECTED
.with_label_values(&[rejection.as_str()])
.inc_by(refused.len() as u64);
applied.rejected += refused.len();
applied.outcome += table.withdraw(peer, refused);
};
if let Err(rejection) = path_verdict {
refuse(nlri, rejection);
return Ok(());
}
let accepted = sift(import.policy, nlri, &mut refuse);
if !accepted.is_empty() {
applied.outcome += table.announce(peer, &attributes(next_hop)?, &accepted);
}
Ok(())
};
// Routes in the body have their next hop in the NEXT_HOP attribute,
// which is one of the set's attributes; routes in MP_REACH_NLRI bring
// theirs along.
announce(&update.announced_routes, None)?;
for attribute in &update.path_attributes {
if let PathAttribute::MpReachNlri(mp) = attribute {
announce(&mp.prefixes, Some(&mp.next_hop))?;
}
}
Ok(applied)
}
/// The routes of `nlri` whose prefixes the policy takes; the others are
/// given to `refuse`. Nearly always that is all of them, and nothing is
/// copied.
fn sift<'a>(
policy: &ImportPolicy,
nlri: &'a [Nlri],
refuse: &mut impl FnMut(&[Nlri], Rejection),
) -> Cow<'a, [Nlri]> {
let verdict = |route: &Nlri| policy.check_prefix(&route.prefix);
let Some(first_refused) = nlri.iter().position(|route| verdict(route).is_err()) else {
return Cow::Borrowed(nlri);
};
let mut accepted = nlri[..first_refused].to_vec();
for route in &nlri[first_refused..] {
match verdict(route) {
Ok(()) => accepted.push(*route),
Err(rejection) => refuse(std::slice::from_ref(route), rejection),
}
}
Cow::Owned(accepted)
}