Received routes are listed from the best down: the RIB gains ranked_routes, which runs selection again without the winners, so the second route is the one that would be installed were the best withdrawn. The API gives each route its rank, and the page shows it as a numbered badge after the star. The lookup box moves to the received-routes table, which is what it filters, and takes an AS number: a new `origin` query parameter finds every route whose path ends in that AS, with an empty path counting as the local AS. Long cells are clamped to a few lines with a control to open them, and an AS repeated along a path (prepending) is shown once with a ×N badge. Peer names are bold and dark, not underlined links, so a row can be found by its name among the addresses and numbers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
282 lines
10 KiB
Rust
282 lines
10 KiB
Rust
//! Choosing the best route for a prefix (RFC 4271 §9.1.2.2).
|
|
//!
|
|
//! The steps, in order. A route wins at the first step where it differs:
|
|
//!
|
|
//! 1. highest LOCAL_PREF;
|
|
//! 2. shortest AS path;
|
|
//! 3. lowest ORIGIN;
|
|
//! 4. lowest MULTI_EXIT_DISC, among routes from the same neighbouring AS;
|
|
//! 5. a route from an external peer over one from an internal peer;
|
|
//! 6. the peer with the lowest BGP Identifier;
|
|
//! 7. the peer with the lowest address.
|
|
//!
|
|
//! (The RFC's step between 5 and 6, the interior cost to the next hop, has
|
|
//! nothing to compare until there is an IGP to ask.)
|
|
//!
|
|
//! Steps 1 to 3 are put into one number per route when it is stored,
|
|
//! [`preference`]. Most of the time that number alone settles it: a new
|
|
//! route with a larger one than the best is the new best, with a smaller
|
|
//! one changes nothing, and neither needs a look at any other route. Only a
|
|
//! tie goes on to [`choose`], which looks at the whole row, and that begins
|
|
//! with a pass over the numbers to find the routes still in the running.
|
|
//!
|
|
//! [`after_add`], [`after_replace`] and [`after_remove`] are what the table
|
|
//! calls when a row has changed. Each says whether the best route is now
|
|
//! another, and takes the short way where there is one. They rely on, and
|
|
//! keep, one fact about a row: its best route has the highest preference
|
|
//! of all its routes.
|
|
|
|
use super::attrs::SelectionKey;
|
|
use super::{DEFAULT_LOCAL_PREF, Path, PeerInfo, Row};
|
|
|
|
/// Steps 1 to 3 as one number of 48 bits: the larger, the better.
|
|
///
|
|
/// LOCAL_PREF fills the high 32 bits. Below it come the AS path length in
|
|
/// 12 bits and the ORIGIN in 2, each turned upside down so that shorter and
|
|
/// lower compare as larger. Twelve bits are enough for any path: a BGP
|
|
/// message of 4096 octets has room for about a thousand AS numbers.
|
|
pub(super) fn preference(key: &SelectionKey) -> u64 {
|
|
// LOCAL_PREF is a matter between routers of one AS. What an external
|
|
// peer sent as one never gets as far as the table (the decoder drops
|
|
// it, RFC 7606 §7.5); a route from such a peer that has one was given
|
|
// it by our own import policy, which is who is to say.
|
|
let local_pref = key.local_pref.unwrap_or(DEFAULT_LOCAL_PREF);
|
|
const LONGEST: u16 = (1 << 12) - 1;
|
|
(u64::from(local_pref) << 16)
|
|
| (u64::from(LONGEST - key.as_path_len.min(LONGEST)) << 4)
|
|
| (u64::from(3 - key.origin.min(3)) << 2)
|
|
}
|
|
|
|
/// Buffers that [`choose`] reuses.
|
|
#[derive(Default)]
|
|
pub(super) struct Scratch {
|
|
/// Routes still in the running: (neighbouring AS, MED, index in row).
|
|
candidates: Vec<(u32, u32, u32)>,
|
|
}
|
|
|
|
/// The best route changed after a route was added at the end of `row`?
|
|
pub(super) fn after_add(row: &mut Row, peers: &[Option<PeerInfo>], scratch: &mut Scratch) -> bool {
|
|
let added = row.paths.len() - 1;
|
|
let preference = row.paths[added].preference();
|
|
let best = row.paths[row.best()].preference();
|
|
// Better than the best in the first steps of selection is better than
|
|
// everything; worse than it changes nothing. A tie has to be settled by
|
|
// the later steps.
|
|
if preference > best {
|
|
row.set_best(added);
|
|
true
|
|
} else if preference < best {
|
|
false
|
|
} else if let Some(wins) = settles_tie(row, added, peers) {
|
|
if wins {
|
|
row.set_best(added);
|
|
}
|
|
wins
|
|
} else {
|
|
choose(row, peers, scratch)
|
|
}
|
|
}
|
|
|
|
/// The best route changed after the route at `at` was replaced by another
|
|
/// from the same peer? `before` is the preference of the route it replaced.
|
|
/// The two are known to differ.
|
|
pub(super) fn after_replace(
|
|
row: &mut Row,
|
|
at: usize,
|
|
before: u64,
|
|
peers: &[Option<PeerInfo>],
|
|
scratch: &mut Scratch,
|
|
) -> bool {
|
|
let preference = row.paths[at].preference();
|
|
if at == row.best() {
|
|
// The best route itself changed. If it got better in the first
|
|
// steps it is still the best, but it is a different route now.
|
|
if preference <= before {
|
|
choose(row, peers, scratch);
|
|
}
|
|
return true;
|
|
}
|
|
let best = row.paths[row.best()].preference();
|
|
if preference > best {
|
|
row.set_best(at);
|
|
true
|
|
} else if preference < best && before < best {
|
|
// It was not in the running and still is not.
|
|
false
|
|
} else {
|
|
choose(row, peers, scratch)
|
|
}
|
|
}
|
|
|
|
/// Takes the route at `at` out of `row`, which has others, and returns
|
|
/// whether the best route changed.
|
|
pub(super) fn after_remove(
|
|
row: &mut Row,
|
|
at: usize,
|
|
peers: &[Option<PeerInfo>],
|
|
scratch: &mut Scratch,
|
|
) -> bool {
|
|
let best = row.best();
|
|
let best_preference = row.paths[best].preference();
|
|
let last = row.paths.len() - 1;
|
|
let removed = row.swap_remove(at);
|
|
if at == best {
|
|
choose(row, peers, scratch);
|
|
return true;
|
|
}
|
|
// The last route moved into the gap.
|
|
if best == last {
|
|
row.set_best(at);
|
|
}
|
|
// A route that tied with the best in the first steps may have been
|
|
// what kept another from being best, by its MED.
|
|
removed.preference() == best_preference && choose(row, peers, scratch)
|
|
}
|
|
|
|
/// Chooses the best route of `row` from all its routes, records it in the
|
|
/// row, and returns whether it is another route than before.
|
|
///
|
|
/// The row must have at least one route.
|
|
fn choose(row: &mut Row, peers: &[Option<PeerInfo>], scratch: &mut Scratch) -> bool {
|
|
let before = row.best;
|
|
let top = row
|
|
.paths
|
|
.iter()
|
|
.map(|path| path.preference())
|
|
.max()
|
|
.unwrap_or(0);
|
|
|
|
let candidates = &mut scratch.candidates;
|
|
candidates.clear();
|
|
for (index, path) in row.paths.iter().enumerate() {
|
|
if path.preference() == top {
|
|
let key = path.attrs.key();
|
|
// A route without a MED is taken to have the lowest (RFC 4271
|
|
// §9.1.2.2 c).
|
|
candidates.push((key.neighbor_as, key.med.unwrap_or(0), index as u32));
|
|
}
|
|
}
|
|
|
|
// Step 4. MEDs are compared only between routes from the same
|
|
// neighbouring AS, so "lower MED" is not an order over all the routes,
|
|
// and comparing them pair by pair can go round in circles. Instead:
|
|
// within each neighbouring AS only the routes with its lowest MED stay
|
|
// in, and the steps after are an order over what is left.
|
|
candidates.sort_unstable();
|
|
let mut best: Option<(TieBreak, u32)> = None;
|
|
let mut group = None;
|
|
for &(neighbor_as, med, index) in candidates.iter() {
|
|
// Sorted by AS and then MED: the first of each AS has its lowest.
|
|
let lowest = match group {
|
|
Some((group_as, lowest)) if group_as == neighbor_as => lowest,
|
|
_ => {
|
|
group = Some((neighbor_as, med));
|
|
med
|
|
}
|
|
};
|
|
if med != lowest {
|
|
continue;
|
|
}
|
|
let tie_break = TieBreak::of(row, index as usize, peers);
|
|
if best.as_ref().is_none_or(|(least, _)| tie_break < *least) {
|
|
best = Some((tie_break, index));
|
|
}
|
|
}
|
|
|
|
row.best = best.map_or(0, |(_, index)| index);
|
|
row.best != before
|
|
}
|
|
|
|
/// The routes of `row` from the best down, as indices into its routes:
|
|
/// after the best, the one that would be chosen were the best gone, and
|
|
/// so on. What an operator asks when looking at a prefix: which route
|
|
/// would take over.
|
|
pub(super) fn ranked(row: &Row, peers: &[Option<PeerInfo>]) -> Vec<usize> {
|
|
// A copy to take the winners out of one by one, and where each of its
|
|
// routes is in the row itself.
|
|
let mut rest = Row {
|
|
prefix: row.prefix,
|
|
best: 0,
|
|
seqs: row.seqs,
|
|
paths: row
|
|
.paths
|
|
.iter()
|
|
.map(|path| Path {
|
|
word: path.word,
|
|
attrs: std::sync::Arc::clone(&path.attrs),
|
|
})
|
|
.collect(),
|
|
path_ids: row.path_ids.clone(),
|
|
};
|
|
let mut places: Vec<usize> = (0..row.paths.len()).collect();
|
|
let mut scratch = Scratch::default();
|
|
let mut order = Vec::with_capacity(row.paths.len());
|
|
while !rest.paths.is_empty() {
|
|
choose(&mut rest, peers, &mut scratch);
|
|
let at = rest.best();
|
|
order.push(places[at]);
|
|
rest.swap_remove(at);
|
|
places.swap_remove(at);
|
|
}
|
|
order
|
|
}
|
|
|
|
/// For a route just added at `added` that ties with the best in steps 1 to
|
|
/// 3: whether it is the new best, if that can be said from the two routes
|
|
/// alone. `None` if the whole row has to be looked at.
|
|
///
|
|
/// It can be said when MED cannot knock either of them out. The new route
|
|
/// has the lowest MED there is, so nothing knocks it out. It knocks out
|
|
/// only routes of its own neighbouring AS with a higher MED, and if the
|
|
/// best is not one of those, the best is still the best of all the others.
|
|
/// So the better of the two is the best of all. This is the usual case:
|
|
/// most routes have no MED.
|
|
fn settles_tie(row: &Row, added: usize, peers: &[Option<PeerInfo>]) -> Option<bool> {
|
|
let new = row.paths[added].attrs.key();
|
|
let best = row.paths[row.best()].attrs.key();
|
|
if new.med.unwrap_or(0) != 0 {
|
|
return None;
|
|
}
|
|
if best.neighbor_as == new.neighbor_as && best.med.unwrap_or(0) != 0 {
|
|
return None;
|
|
}
|
|
Some(TieBreak::of(row, added, peers) < TieBreak::of(row, row.best(), peers))
|
|
}
|
|
|
|
/// Steps 5 to 7, then the peer's number, and then the path identifier for
|
|
/// a peer that sent several routes: so that the choice never depends on the
|
|
/// order routes arrived in. The least wins.
|
|
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
|
struct TieBreak {
|
|
internal: bool,
|
|
router_id: u32,
|
|
address: std::net::IpAddr,
|
|
peer: u16,
|
|
path_id: u32,
|
|
}
|
|
|
|
impl TieBreak {
|
|
fn of(row: &Row, index: usize, peers: &[Option<PeerInfo>]) -> Self {
|
|
let peer = row.paths[index].peer();
|
|
let path_id = row.path_id(index);
|
|
match peers.get(peer.as_usize()).copied().flatten() {
|
|
Some(info) => Self {
|
|
internal: info.ibgp,
|
|
router_id: info.router_id.into(),
|
|
address: info.address,
|
|
peer: peer.0,
|
|
path_id,
|
|
},
|
|
// Not expected: a peer is forgotten only once its routes are
|
|
// gone. If it happened, anything known would be preferred.
|
|
None => Self {
|
|
internal: true,
|
|
router_id: u32::MAX,
|
|
address: std::net::Ipv6Addr::from(u128::MAX).into(),
|
|
peer: peer.0,
|
|
path_id,
|
|
},
|
|
}
|
|
}
|
|
}
|