Tidy the export limits, passive peers and status page code

Readability, no change in behaviour, but for one log line:

- The routes a peer holds from us were counted twice over, differently,
  in the exporter: once per learned-route batch and once per originated
  route. Learned::held_by_peer counts the learned ones in one place, and
  ExportLimits::exceeded_by returns the limit that was passed instead of a
  bool, so that ending the session no longer has to dig it out again with
  a default that could not happen.
- The two hold-downs for a limit passed, ours or the peer's, were two
  near-identical blocks; they are one match.
- How a session ended is put into words in one place
  (SessionEnd::reason), for the dialer's backoff and the passive peer's
  state alike.
- parse_export_limits parses one block; what a missing block means (no
  limits at the top, the top's limits for a peer) is said where it
  differs, instead of Option in and Option out.
- The startup warnings about export limits are a function of their own,
  and the peers without limits are named in one line rather than one
  line each: a router with eighty sessions does not need eighty warnings.
  The warning about a peer without a role is gone: the configuration now
  refuses one.
- Hex formatting of attribute octets is one function; the text for an
  unknown attribute is built in three plain steps.
- The exporter's test fixture returns a named struct, not a tuple.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-06 10:05:31 +02:00
1 parent 2dfc00862b
commit befc292215
9 files changed
+219 -214

No files matched your search

+52 -46
View File
@@ -54,7 +54,7 @@ use crate::export::{self, Egress, Outgoing, Reflection, Source};
use crate::family::FamilySet; use crate::family::FamilySet;
use crate::marks::Control; use crate::marks::Control;
use crate::metrics::ROUTES_EXPORTED; use crate::metrics::ROUTES_EXPORTED;
use crate::policy::{ExportLimits, PeerRole}; use crate::policy::PeerRole;
use crate::session::{PeerId, SessionClosed, SessionEvent, SessionHandle, SessionRegistry}; use crate::session::{PeerId, SessionClosed, SessionEvent, SessionHandle, SessionRegistry};
/// Starts the advertiser. It hears of every session that comes up after /// Starts the advertiser. It hears of every session that comes up after
@@ -292,6 +292,12 @@ struct Learned {
} }
impl Learned { impl Learned {
/// How many learned routes the peer holds from us: those announced,
/// and those not heard of again yet since the table was reset.
fn held_by_peer(&self) -> usize {
self.announced.len() + self.stale.as_ref().map_or(0, HashSet::len)
}
fn new(reader: Reader) -> Self { fn new(reader: Reader) -> Self {
Self { Self {
reader, reader,
@@ -620,15 +626,11 @@ impl Exporter {
withdrawn[version_index(prefix.address_family())].push(prefix.into()); withdrawn[version_index(prefix.address_family())].push(prefix.into());
} }
} }
// What the peer would have once this goes out: none of it goes if // What the peer would hold once this goes out: none of it goes if
// that is more than it may. // that is more than it may.
let routes = self.announced.len() let routes = self.announced.len() + learned.held_by_peer();
+ learned.announced.len() if let Some(limit) = limits.and_then(|limits| limits.exceeded_by(routes)) {
+ learned.stale.as_ref().map_or(0, HashSet::len); return Err(over_limit(session, limit, routes));
if let Some(limits) = limits
&& limits.exceeded_by(routes)
{
return Err(over_limit(session, limits, routes));
} }
for (version, routes) in [IpVersion::Ipv4, IpVersion::Ipv6] for (version, routes) in [IpVersion::Ipv4, IpVersion::Ipv6]
.into_iter() .into_iter()
@@ -691,24 +693,21 @@ impl Exporter {
); );
return Ok(()); return Ok(());
} }
if let Some(limits) = session.export_limits.as_deref() { let limits = session.export_limits.as_deref();
if !limits.allows(&route.prefix) { if limits.is_some_and(|limits| !limits.allows(&route.prefix)) {
debug!( debug!(
peer = %session.peer, peer = %session.peer,
prefix = %route.prefix, prefix = %route.prefix,
"Not announcing route: its prefix is not in the peer's export limits" "Not announcing route: its prefix is not in the peer's export limits"
); );
return Ok(()); return Ok(());
} }
let learned = self.learned.as_ref().map_or(0, |learned| { // What the peer would hold with this route.
learned.announced.len() + learned.stale.as_ref().map_or(0, HashSet::len) let routes = self.announced.len()
}); + usize::from(!self.announced.contains(&route.prefix))
let routes = self.announced.len() + self.learned.as_ref().map_or(0, Learned::held_by_peer);
+ usize::from(!self.announced.contains(&route.prefix)) if let Some(limit) = limits.and_then(|limits| limits.exceeded_by(routes)) {
+ learned; return Err(over_limit(session, limit, routes));
if limits.exceeded_by(routes) {
return Err(over_limit(session, limits, routes));
}
} }
let towards = self.towards(); let towards = self.towards();
let Some(mut update) = build_announce(route, &towards) else { let Some(mut update) = build_announce(route, &towards) else {
@@ -765,10 +764,9 @@ impl Exporter {
} }
} }
/// Ends `session` because `routes` routes were to be announced on it, more /// Ends `session` because `routes` routes were to be announced on it, over
/// than `limits` allow; none of them has been. /// its export limit of `limit`; none of them has been.
fn over_limit(session: &SessionHandle, limits: &ExportLimits, routes: usize) -> Finished { fn over_limit(session: &SessionHandle, limit: u32, routes: usize) -> Finished {
let limit = limits.max_prefixes.unwrap_or_default();
error!( error!(
peer = %session.peer, peer = %session.peer,
name = session.name.as_deref().unwrap_or_default(), name = session.name.as_deref().unwrap_or_default(),
@@ -1644,11 +1642,7 @@ mod tests {
prefixes: &[IpPrefix], prefixes: &[IpPrefix],
originated: &[IpPrefix], originated: &[IpPrefix],
limits: ExportLimits, limits: ExportLimits,
) -> ( ) -> LimitedPeer {
mpsc::Receiver<Message>,
mpsc::UnboundedReceiver<SessionError>,
SessionGuard,
) {
let registry = SessionRegistry::new(); let registry = SessionRegistry::new();
let rib = Arc::new(RibTable::new(4)); let rib = Arc::new(RibTable::new(4));
let local_rib = LocalRib::spawn(); let local_rib = LocalRib::spawn();
@@ -1720,7 +1714,19 @@ mod tests {
}); });
handle.export_limits = Some(Arc::new(limits)); handle.export_limits = Some(Arc::new(limits));
let ending = handle.listen_for_end(); let ending = handle.listen_for_end();
(outbound, ending, registry.register(handle)) LimitedPeer {
outbound,
ending,
_registration: registry.register(handle),
}
}
/// The far end of the limited peer's session: what it is sent, and
/// what ends it.
struct LimitedPeer {
outbound: mpsc::Receiver<Message>,
ending: mpsc::UnboundedReceiver<SessionError>,
_registration: SessionGuard,
} }
/// The routes announced on a session's queue until it has been quiet /// The routes announced on a session's queue until it has been quiet
@@ -1764,17 +1770,17 @@ mod tests {
prefixes: allowing(&[nth_prefix(2), nth_prefix(11)]), prefixes: allowing(&[nth_prefix(2), nth_prefix(11)]),
max_prefixes: None, max_prefixes: None,
}; };
let (mut outbound, mut ending, _registration) = limited( let mut peer = limited(
&[nth_prefix(1), nth_prefix(2), nth_prefix(3)], &[nth_prefix(1), nth_prefix(2), nth_prefix(3)],
&[nth_prefix(10), nth_prefix(11)], &[nth_prefix(10), nth_prefix(11)],
limits, limits,
) )
.await; .await;
assert_eq!( assert_eq!(
announced(&mut outbound).await, announced(&mut peer.outbound).await,
HashSet::from([nth_prefix(2), nth_prefix(11)]) HashSet::from([nth_prefix(2), nth_prefix(11)])
); );
assert!(ending.try_recv().is_err(), "the session goes on"); assert!(peer.ending.try_recv().is_err(), "the session goes on");
} }
/// When more routes would go to a peer than its export limit allows, /// When more routes would go to a peer than its export limit allows,
@@ -1787,8 +1793,8 @@ mod tests {
prefixes: None, prefixes: None,
max_prefixes: Some(3), max_prefixes: Some(3),
}; };
let (mut outbound, mut ending, _registration) = limited(&prefixes, &[], limits).await; let mut peer = limited(&prefixes, &[], limits).await;
let why = tokio::time::timeout(Duration::from_secs(10), ending.recv()) let why = tokio::time::timeout(Duration::from_secs(10), peer.ending.recv())
.await .await
.expect("the session is asked to end") .expect("the session is asked to end")
.expect("by the exporter"); .expect("by the exporter");
@@ -1802,7 +1808,7 @@ mod tests {
), ),
"{why:?}" "{why:?}"
); );
assert_eq!(announced(&mut outbound).await, HashSet::new()); assert_eq!(announced(&mut peer.outbound).await, HashSet::new());
} }
/// Our own routes count too, and none goes past the limit: those that /// Our own routes count too, and none goes past the limit: those that
@@ -1814,8 +1820,8 @@ mod tests {
prefixes: None, prefixes: None,
max_prefixes: Some(2), max_prefixes: Some(2),
}; };
let (mut outbound, mut ending, _registration) = limited(&[], &originated, limits).await; let mut peer = limited(&[], &originated, limits).await;
let why = tokio::time::timeout(Duration::from_secs(10), ending.recv()) let why = tokio::time::timeout(Duration::from_secs(10), peer.ending.recv())
.await .await
.expect("the session is asked to end") .expect("the session is asked to end")
.expect("by the exporter"); .expect("by the exporter");
@@ -1829,6 +1835,6 @@ mod tests {
), ),
"{why:?}" "{why:?}"
); );
assert_eq!(announced(&mut outbound).await.len(), 2); assert_eq!(announced(&mut peer.outbound).await.len(), 2);
} }
} }
+14 -14
View File
@@ -157,9 +157,12 @@ impl Config {
Some(export) => parse_export(export)?, Some(export) => parse_export(export)?,
None => ExportPolicy::default(), None => ExportPolicy::default(),
}); });
let export_limits = parse_export_limits(proto.export_limits.as_ref()) let export_limits = match proto.export_limits.as_ref() {
.map_err(ConfigError::ExportLimits)? Some(limits) => parse_export_limits(limits)
.map(Arc::new); .map_err(ConfigError::ExportLimits)?
.map(Arc::new),
None => None,
};
let graceful_restart = match proto.graceful_restart.as_ref() { let graceful_restart = match proto.graceful_restart.as_ref() {
Some(own) => GracefulRestartConfig::default().overridden_by(own)?, Some(own) => GracefulRestartConfig::default().overridden_by(own)?,
None => GracefulRestartConfig::default(), None => GracefulRestartConfig::default(),
@@ -171,8 +174,8 @@ impl Config {
peer: name.clone(), peer: name.clone(),
problem, problem,
}; };
let peer = let peer = parse_peer(peer, &export, export_limits.as_ref(), graceful_restart)
parse_peer(peer, &export, &export_limits, graceful_restart).map_err(rejected)?; .map_err(rejected)?;
// RFC 9234 is about what networks are to each other. // RFC 9234 is about what networks are to each other.
if peer.role.is_some() && peer.remote_asn == proto.local_asn { if peer.role.is_some() && peer.remote_asn == proto.local_asn {
return Err(rejected(PeerProblem::RoleInOwnAs)); return Err(rejected(PeerProblem::RoleInOwnAs));
@@ -330,14 +333,11 @@ fn parse_export(export: &proto::ExportPolicy) -> Result<ExportPolicy, ExportProb
}) })
} }
/// Limits on what is announced; `None` if there are none, as when the /// Limits on what is announced; `None` for a block that says nothing,
/// message is left out or says nothing. /// which lifts them.
fn parse_export_limits( fn parse_export_limits(
limits: Option<&proto::ExportLimits>, limits: &proto::ExportLimits,
) -> Result<Option<ExportLimits>, ImportProblem> { ) -> Result<Option<ExportLimits>, ImportProblem> {
let Some(limits) = limits else {
return Ok(None);
};
let limits = ExportLimits { let limits = ExportLimits {
prefixes: if limits.prefixes.is_empty() { prefixes: if limits.prefixes.is_empty() {
None None
@@ -352,7 +352,7 @@ fn parse_export_limits(
fn parse_peer( fn parse_peer(
peer: Peer, peer: Peer,
export: &Arc<ExportPolicy>, export: &Arc<ExportPolicy>,
export_limits: &Option<Arc<ExportLimits>>, export_limits: Option<&Arc<ExportLimits>>,
graceful_restart: GracefulRestartConfig, graceful_restart: GracefulRestartConfig,
) -> Result<PeerConfig, PeerProblem> { ) -> Result<PeerConfig, PeerProblem> {
let ep = peer.endpoint.as_ref().ok_or(PeerProblem::MissingEndpoint)?; let ep = peer.endpoint.as_ref().ok_or(PeerProblem::MissingEndpoint)?;
@@ -447,10 +447,10 @@ fn parse_peer(
None => Arc::clone(export), None => Arc::clone(export),
}, },
export_limits: match peer.export_limits.as_ref() { export_limits: match peer.export_limits.as_ref() {
Some(own) => parse_export_limits(Some(own)) Some(own) => parse_export_limits(own)
.map_err(PeerProblem::ExportLimits)? .map_err(PeerProblem::ExportLimits)?
.map(Arc::new), .map(Arc::new),
None => export_limits.clone(), None => export_limits.cloned(),
}, },
graceful_restart: match peer.graceful_restart.as_ref() { graceful_restart: match peer.graceful_restart.as_ref() {
Some(own) => graceful_restart.overridden_by(own)?, Some(own) => graceful_restart.overridden_by(own)?,
+28 -22
View File
@@ -188,6 +188,19 @@ pub struct SessionEnd {
pub result: Result<(), SessionError>, pub result: Result<(), SessionError>,
} }
/// Why a session ended when nothing went wrong.
pub const CLOSED_AT_OUR_REQUEST: &str = "session closed at our request";
impl SessionEnd {
/// Why the session ended, in a sentence.
pub fn reason(&self) -> String {
match &self.result {
Ok(()) => CLOSED_AT_OUR_REQUEST.to_string(),
Err(e) => e.to_string(),
}
}
}
/// Runs the BGP session on `stream` until it ends. `direction` is which /// Runs the BGP session on `stream` until it ends. `direction` is which
/// side opened the connection. /// side opened the connection.
pub async fn run_session( pub async fn run_session(
@@ -214,28 +227,21 @@ pub async fn run_session(
}; };
let established = session.established.is_some(); let established = session.established.is_some();
let hold_downs = &session.server.hold_downs; let hold_downs = &session.server.hold_downs;
if matches!(result, Err(SessionError::PrefixLimitExceeded { .. })) { // A peer that sent far more than it should, or that we were to send
// A peer that sends far more than it should will do so again on // far more than we should, will be so again on the next session; it
// the next session; it is left alone for a while. // is left alone for a while, until it or we are put right.
hold_downs.hold( let over_limit = match &result {
peer_addr.into(), Err(SessionError::PrefixLimitExceeded { .. }) => {
session.import.hold_down, Some((HeldDown::OverLimit, &*PREFIX_LIMIT_EXCEEDED))
HeldDown::OverLimit, }
); Err(SessionError::ExportLimitExceeded { .. }) => {
PREFIX_LIMIT_EXCEEDED Some((HeldDown::OverExportLimit, &*EXPORT_LIMIT_EXCEEDED))
.with_label_values(&[session.refusals.label()]) }
.inc(); _ => None,
} };
if matches!(result, Err(SessionError::ExportLimitExceeded { .. })) { if let Some((why, exceeded)) = over_limit {
// Whatever made us want to send too much will be there on the hold_downs.hold(peer_addr.into(), session.import.hold_down, why);
// next session too. Until it is put right, the peer is told of exceeded
// nothing rather than of too much.
hold_downs.hold(
peer_addr.into(),
session.import.hold_down,
HeldDown::OverExportLimit,
);
EXPORT_LIMIT_EXCEEDED
.with_label_values(&[session.refusals.label()]) .with_label_values(&[session.refusals.label()])
.inc(); .inc();
} }
+3 -3
View File
@@ -18,7 +18,7 @@ use tracing::{info, warn};
use crate::collision::Direction; use crate::collision::Direction;
use crate::config::PeerConfig; use crate::config::PeerConfig;
use crate::connection::{PeerSettings, ServerContext, run_session}; use crate::connection::{CLOSED_AT_OUR_REQUEST, PeerSettings, ServerContext, run_session};
use crate::session::DialState; use crate::session::DialState;
/// Upper bound for exponential backoff between connection attempts. /// Upper bound for exponential backoff between connection attempts.
@@ -121,7 +121,7 @@ async fn dial_loop(peer: PeerConfig, server: Arc<ServerContext>, connect_retry:
if end.established { if end.established {
backoff = connect_retry; backoff = connect_retry;
} }
end.result.map_err(|e| e.to_string()) end.result.is_ok().then_some(()).ok_or_else(|| end.reason())
} }
Err(e) => Err(format!("connection lost at once: {e}")), Err(e) => Err(format!("connection lost at once: {e}")),
}, },
@@ -130,7 +130,7 @@ async fn dial_loop(peer: PeerConfig, server: Arc<ServerContext>, connect_retry:
}; };
let reason = match ended { let reason = match ended {
Ok(()) => "session closed at our request".to_string(), Ok(()) => CLOSED_AT_OUR_REQUEST.to_string(),
Err(reason) => { Err(reason) => {
warn!(peer = %peer.name, endpoint = %peer.endpoint, err = %reason, "Session attempt ended"); warn!(peer = %peer.name, endpoint = %peer.endpoint, err = %reason, "Session attempt ended");
reason reason
+7 -7
View File
@@ -296,10 +296,10 @@ impl ExportLimits {
.is_none_or(|prefixes| prefixes.allows(prefix)) .is_none_or(|prefixes| prefixes.allows(prefix))
} }
/// Whether `routes` routes at a time are more than may be announced. /// The limit that `routes` routes at a time would go over, if they
pub fn exceeded_by(&self, routes: usize) -> bool { /// would.
self.max_prefixes pub fn exceeded_by(&self, routes: usize) -> Option<u32> {
.is_some_and(|limit| routes > limit as usize) self.max_prefixes.filter(|limit| routes > *limit as usize)
} }
} }
@@ -1391,10 +1391,10 @@ mod tests {
prefixes: None, prefixes: None,
max_prefixes: Some(2), max_prefixes: Some(2),
}; };
assert!(!limits.exceeded_by(2)); assert_eq!(limits.exceeded_by(2), None);
assert!(limits.exceeded_by(3)); assert_eq!(limits.exceeded_by(3), Some(2));
assert!(limits.allows(&IpPrefix::try_from("203.0.113.0/24").unwrap())); assert!(limits.allows(&IpPrefix::try_from("203.0.113.0/24").unwrap()));
assert!(!ExportLimits::default().exceeded_by(usize::MAX)); assert_eq!(ExportLimits::default().exceeded_by(usize::MAX), None);
} }
#[test] #[test]
+60 -61
View File
@@ -21,7 +21,7 @@ use bgp_rpki::Rpki;
use crate::address::{PeerAddress, socket_text}; use crate::address::{PeerAddress, socket_text};
use crate::advertise::spawn_advertiser; use crate::advertise::spawn_advertiser;
use crate::collision::{CollisionTable, Direction}; use crate::collision::{CollisionTable, Direction};
use crate::config::Config; use crate::config::{Config, PeerConfig};
use crate::connection::{PeerSettings, ServerContext, run_session}; use crate::connection::{PeerSettings, ServerContext, run_session};
use crate::dialer::spawn_dialers; use crate::dialer::spawn_dialers;
use crate::error::ServerError; use crate::error::ServerError;
@@ -198,56 +198,8 @@ impl BgpServer {
"No limit on the routes taken from this peer (import.max_prefixes)" "No limit on the routes taken from this peer (import.max_prefixes)"
); );
} }
// Without a role there is no rule about which learned routes
// go to the peer: all of them do.
if peer.export.learned_routes
&& peer.role.is_none()
&& peer.remote_asn != self.config.local_asn
{
warn!(
peer = %peer.name,
"Every route learned from any peer is passed on to this one: it has no role \
(export.learned_routes without role)"
);
}
}
// The export limits that hold for some peer, each once, with the
// peers they hold for.
let mut limited: Vec<(&ExportLimits, Vec<&str>)> = Vec::new();
for peer in &self.config.peers {
let ibgp = peer.remote_asn == self.config.local_asn;
if !ExportLimits::apply_to(peer.role, ibgp) {
continue;
}
let Some(limits) = peer.export_limits.as_deref() else {
warn!(
peer = %peer.name,
"Nothing limits what this peer is told (export_limits)"
);
continue;
};
match limited
.iter_mut()
.find(|(seen, _)| std::ptr::eq(*seen, limits))
{
Some((_, names)) => names.push(&peer.name),
None => limited.push((limits, vec![&peer.name])),
}
}
// A route of our own that the limits leave out is likely a mistake
// in one or the other.
for route in &self.config.originate {
for (limits, names) in &limited {
if !limits.allows(&route.prefix) {
warn!(
prefix = %route.prefix,
peers = names.join(", "),
"An originated route is not announced to these peers: export_limits.prefixes \
leaves it out"
);
}
}
} }
warn_about_export_limits(&self.config);
if self.config.accept_unconfigured_peers { if self.config.accept_unconfigured_peers {
warn!("Taking sessions and routes from any host (accept_unconfigured_peers)"); warn!("Taking sessions and routes from any host (accept_unconfigured_peers)");
} }
@@ -425,6 +377,54 @@ pub async fn close_sessions(registry: &SessionRegistry) {
} }
} }
/// Says what the export limits leave undone: the peers they are for that
/// have none, and the routes of our own that they leave out, which is
/// likely a mistake in one or the other.
fn warn_about_export_limits(config: &Config) {
let limited: Vec<&PeerConfig> = config
.peers
.iter()
.filter(|peer| ExportLimits::apply_to(peer.role, peer.remote_asn == config.local_asn))
.collect();
let unlimited: Vec<&str> = limited
.iter()
.filter(|peer| peer.export_limits.is_none())
.map(|peer| peer.name.as_str())
.collect();
if !unlimited.is_empty() {
warn!(
peers = unlimited.join(", "),
"Nothing limits what these peers are told (export_limits)"
);
}
// Each distinct set of limits once, with the peers that have it.
let mut distinct: Vec<(&Arc<ExportLimits>, Vec<&str>)> = Vec::new();
for peer in &limited {
let Some(limits) = &peer.export_limits else {
continue;
};
match distinct
.iter_mut()
.find(|(seen, _)| Arc::ptr_eq(seen, limits))
{
Some((_, peers)) => peers.push(&peer.name),
None => distinct.push((limits, vec![&peer.name])),
}
}
for route in &config.originate {
for (limits, peers) in &distinct {
if !limits.allows(&route.prefix) {
warn!(
prefix = %route.prefix,
peers = peers.join(", "),
"An originated route is not announced to these peers: export_limits.prefixes \
leaves it out"
);
}
}
}
}
/// Opens a listening socket on `addr`. /// Opens a listening socket on `addr`.
fn listen(addr: SocketAddr) -> io::Result<TcpListener> { fn listen(addr: SocketAddr) -> io::Result<TcpListener> {
let socket = match addr { let socket = match addr {
@@ -480,23 +480,22 @@ async fn serve(
peer: PeerSettings, peer: PeerSettings,
_slot: ConnectionSlot, _slot: ConnectionSlot,
) { ) {
// Nobody dials a passive peer, so its state is said here. // Nobody dials a passive peer, so how its session ended is noted here.
let passive = peer.name.clone().filter(|_| peer.passive); let passive = if peer.passive {
peer.name.clone()
} else {
None
};
let registry = context.registry.clone(); let registry = context.registry.clone();
let end = run_session(stream, peer_addr, Direction::Inbound, context, peer).await; let end = run_session(stream, peer_addr, Direction::Inbound, context, peer).await;
if let Err(e) = &end.result { if let Err(e) = &end.result {
warn!(peer_addr = %socket_text(peer_addr), err = %e, "BGP session terminated with error"); warn!(peer_addr = %socket_text(peer_addr), err = %e, "BGP session terminated with error");
} }
if let Some(name) = passive { if let Some(name) = passive {
registry.set_dial_state( let state = DialState::Passive {
&name, last_error: Some(end.reason()),
DialState::Passive { };
last_error: Some(match end.result { registry.set_dial_state(&name, state);
Ok(()) => "session closed at our request".to_string(),
Err(e) => e.to_string(),
}),
},
);
} }
} }
+45 -51
View File
@@ -327,6 +327,11 @@ fn as_path_text(path: &AsPathAttribute) -> String {
segments.collect::<Vec<_>>().join(" ") segments.collect::<Vec<_>>().join(" ")
} }
/// Octets as hexadecimal digits: `ab01`.
fn hex(octets: &[u8]) -> String {
octets.iter().map(|o| format!("{o:02x}")).collect()
}
/// An extended community (RFC 4360) as text. Route targets and route /// An extended community (RFC 4360) as text. Route targets and route
/// origins, of the three kinds of global administrator, by name; the rest /// origins, of the three kinds of global administrator, by name; the rest
/// as their eight octets. /// as their eight octets.
@@ -335,12 +340,7 @@ fn extended_community_text(community: &ExtendedCommunity) -> String {
let kind = match b[1] { let kind = match b[1] {
0x02 => "rt", 0x02 => "rt",
0x03 => "ro", 0x03 => "ro",
_ => { _ => return format!("0x{}", hex(&b)),
return format!(
"0x{}",
b.iter().map(|o| format!("{o:02x}")).collect::<String>()
);
}
}; };
let u16_at = |at: usize| u16::from_be_bytes([b[at], b[at + 1]]); let u16_at = |at: usize| u16::from_be_bytes([b[at], b[at + 1]]);
let u32_at = |at: usize| u32::from_be_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]]); let u32_at = |at: usize| u32::from_be_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]]);
@@ -348,17 +348,21 @@ fn extended_community_text(community: &ExtendedCommunity) -> String {
0x00 => format!("{kind} {}:{}", u16_at(2), u32_at(4)), 0x00 => format!("{kind} {}:{}", u16_at(2), u32_at(4)),
0x01 => format!("{kind} {}:{}", Ipv4Addr::from(u32_at(2)), u16_at(6)), 0x01 => format!("{kind} {}:{}", Ipv4Addr::from(u32_at(2)), u16_at(6)),
0x02 => format!("{kind} {}:{}", u32_at(2), u16_at(6)), 0x02 => format!("{kind} {}:{}", u32_at(2), u16_at(6)),
_ => format!( _ => format!("0x{}", hex(&b)),
"0x{}",
b.iter().map(|o| format!("{o:02x}")).collect::<String>()
),
} }
} }
/// The name and value of an attribute the summary has no field for, or /// The name and value of an attribute the summary has no field for, or
/// `None` for one it has. /// `None` for one it has.
fn other_attribute(attribute: &PathAttribute) -> Option<OtherAttribute> { fn other_attribute(attribute: &PathAttribute) -> Option<OtherAttribute> {
let (name, value) = match attribute { let other = |name: &str, value: String| {
Some(OtherAttribute {
name: name.to_string(),
value,
})
};
let list = |items: Vec<String>, separator| items.join(separator);
match attribute {
PathAttribute::Origin(_) PathAttribute::Origin(_)
| PathAttribute::AsPath(_) | PathAttribute::AsPath(_)
| PathAttribute::NextHop(_) | PathAttribute::NextHop(_)
@@ -367,60 +371,50 @@ fn other_attribute(attribute: &PathAttribute) -> Option<OtherAttribute> {
| PathAttribute::Communities(_) | PathAttribute::Communities(_)
| PathAttribute::LargeCommunities(_) | PathAttribute::LargeCommunities(_)
| PathAttribute::MpReachNlri(_) | PathAttribute::MpReachNlri(_)
| PathAttribute::MpUnreachNlri(_) => return None, | PathAttribute::MpUnreachNlri(_) => None,
PathAttribute::AtomicAggregate(_) => ("atomic aggregate".to_string(), "set".to_string()), PathAttribute::AtomicAggregate(_) => other("atomic aggregate", "set".to_string()),
PathAttribute::Aggregator(a) => ("aggregator".to_string(), format!("AS{} {}", a.asn, a.ip)), PathAttribute::Aggregator(a) => other("aggregator", format!("AS{} {}", a.asn, a.ip)),
PathAttribute::ExtendedCommunities(e) => ( PathAttribute::ExtendedCommunities(e) => other(
"extended communities".to_string(), "extended communities",
e.extended_communities list(
.iter() e.extended_communities
.map(extended_community_text) .iter()
.collect::<Vec<_>>() .map(extended_community_text)
.join(", "), .collect(),
", ",
),
), ),
PathAttribute::As4Path(p) => ("AS4_PATH".to_string(), as_path_text(p)), PathAttribute::As4Path(p) => other("AS4_PATH", as_path_text(p)),
PathAttribute::OnlyToCustomer(asn) => ("only to customer".to_string(), format!("AS{asn}")), PathAttribute::OnlyToCustomer(asn) => other("only to customer", format!("AS{asn}")),
PathAttribute::OriginatorId(id) => ("originator ID".to_string(), id.to_string()), PathAttribute::OriginatorId(id) => other("originator ID", id.to_string()),
PathAttribute::ClusterList(ids) => ( PathAttribute::ClusterList(ids) => other(
"cluster list".to_string(), "cluster list",
ids.iter() list(ids.iter().map(Ipv4Addr::to_string).collect(), " "),
.map(Ipv4Addr::to_string)
.collect::<Vec<_>>()
.join(" "),
), ),
PathAttribute::UnknownPathAttribute { PathAttribute::UnknownPathAttribute {
flags, flags,
type_code, type_code,
payload, payload,
} => { } => {
let kind = [ // "optional transitive, 2 octets: ab01"
let flags: Vec<&str> = [
(flags.optional(), "optional"), (flags.optional(), "optional"),
(flags.transitive(), "transitive"), (flags.transitive(), "transitive"),
(flags.partial(), "partial"), (flags.partial(), "partial"),
] ]
.into_iter() .into_iter()
.filter_map(|(set, word)| set.then_some(word)) .filter_map(|(set, word)| set.then_some(word))
.collect::<Vec<_>>(); .collect();
let octets = payload let mut value = format!("{} octets", payload.len());
.iter() if !flags.is_empty() {
.map(|o| format!("{o:02x}")) value = format!("{}, {value}", flags.join(" "));
.collect::<String>(); }
( if !payload.is_empty() {
format!("attribute {type_code}"), value = format!("{value}: {}", hex(payload));
format!( }
"{}{} octets{}{octets}", other(&format!("attribute {type_code}"), value)
if kind.is_empty() {
String::new()
} else {
format!("{}, ", kind.join(" "))
},
payload.len(),
if payload.is_empty() { "" } else { ": " },
),
)
} }
}; }
Some(OtherAttribute { name, value })
} }
pub fn summarize(attributes: &[PathAttribute], next_hop: Option<&NlriNextHop>) -> AttrSummary { pub fn summarize(attributes: &[PathAttribute], next_hop: Option<&NlriNextHop>) -> AttrSummary {
+7 -7
View File
@@ -177,14 +177,14 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
if (high === L) return low === L ? 'to every network' : `only to AS${low} (and others named so)`; if (high === L) return low === L ? 'to every network' : `only to AS${low} (and others named so)`;
return ''; return '';
} }
const [admin, fn, arg] = p; const [admin, func, arg] = p;
if (admin !== L) return ''; if (admin !== L) return '';
if (fn === 0) return arg ? `not to AS${arg}` : 'to no other network'; if (func === 0) return arg ? `not to AS${arg}` : 'to no other network';
if (fn === 1) return `only to AS${arg} (and others named so)`; if (func === 1) return `only to AS${arg} (and others named so)`;
if (fn >= 11 && fn <= 13) return `our AS ${TIMES[fn - 10]} more on the path to ${arg ? `AS${arg}` : 'every network'}`; if (func >= 11 && func <= 13) return `our AS ${TIMES[func - 10]} more on the path to ${arg ? `AS${arg}` : 'every network'}`;
if (fn === 20) return `not to group ${arg}`; if (func === 20) return `not to group ${arg}`;
if (fn === 21) return `only to group ${arg} (and others named so)`; if (func === 21) return `only to group ${arg} (and others named so)`;
if (fn >= 31 && fn <= 33) return `our AS ${TIMES[fn - 30]} more on the path to group ${arg}`; if (func >= 31 && func <= 33) return `our AS ${TIMES[func - 30]} more on the path to group ${arg}`;
return ''; return '';
} }
@@ -143,9 +143,9 @@ in hold-down for the neighbor's `import.hold_down_seconds` (default: 600
seconds). Set `max_prefixes` somewhat above the number of routes that the seconds). Set `max_prefixes` somewhat above the number of routes that the
local AS and its customers originate. local AS and its customers originate.
At startup, Ferrix logs a warning for each neighbor to which export limits At startup, Ferrix logs a warning that lists the neighbors to which export
apply but none are configured, and for each originated route that the limits apply but none are configured, and a warning for each originated
`prefixes` of the export limits do not permit. route that the `prefixes` of the export limits do not permit.
### Verifying Export Limits ### Verifying Export Limits