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
+205 -200

No files matched your search

+43 -37
View File
@@ -54,7 +54,7 @@ use crate::export::{self, Egress, Outgoing, Reflection, Source};
use crate::family::FamilySet;
use crate::marks::Control;
use crate::metrics::ROUTES_EXPORTED;
use crate::policy::{ExportLimits, PeerRole};
use crate::policy::PeerRole;
use crate::session::{PeerId, SessionClosed, SessionEvent, SessionHandle, SessionRegistry};
/// Starts the advertiser. It hears of every session that comes up after
@@ -292,6 +292,12 @@ struct 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 {
Self {
reader,
@@ -620,15 +626,11 @@ impl Exporter {
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.
let routes = self.announced.len()
+ learned.announced.len()
+ learned.stale.as_ref().map_or(0, HashSet::len);
if let Some(limits) = limits
&& limits.exceeded_by(routes)
{
return Err(over_limit(session, limits, routes));
let routes = self.announced.len() + learned.held_by_peer();
if let Some(limit) = limits.and_then(|limits| limits.exceeded_by(routes)) {
return Err(over_limit(session, limit, routes));
}
for (version, routes) in [IpVersion::Ipv4, IpVersion::Ipv6]
.into_iter()
@@ -691,8 +693,8 @@ impl Exporter {
);
return Ok(());
}
if let Some(limits) = session.export_limits.as_deref() {
if !limits.allows(&route.prefix) {
let limits = session.export_limits.as_deref();
if limits.is_some_and(|limits| !limits.allows(&route.prefix)) {
debug!(
peer = %session.peer,
prefix = %route.prefix,
@@ -700,15 +702,12 @@ impl Exporter {
);
return Ok(());
}
let learned = self.learned.as_ref().map_or(0, |learned| {
learned.announced.len() + learned.stale.as_ref().map_or(0, HashSet::len)
});
// What the peer would hold with this route.
let routes = self.announced.len()
+ usize::from(!self.announced.contains(&route.prefix))
+ learned;
if limits.exceeded_by(routes) {
return Err(over_limit(session, limits, routes));
}
+ self.learned.as_ref().map_or(0, Learned::held_by_peer);
if let Some(limit) = limits.and_then(|limits| limits.exceeded_by(routes)) {
return Err(over_limit(session, limit, routes));
}
let towards = self.towards();
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
/// than `limits` allow; none of them has been.
fn over_limit(session: &SessionHandle, limits: &ExportLimits, routes: usize) -> Finished {
let limit = limits.max_prefixes.unwrap_or_default();
/// Ends `session` because `routes` routes were to be announced on it, over
/// its export limit of `limit`; none of them has been.
fn over_limit(session: &SessionHandle, limit: u32, routes: usize) -> Finished {
error!(
peer = %session.peer,
name = session.name.as_deref().unwrap_or_default(),
@@ -1644,11 +1642,7 @@ mod tests {
prefixes: &[IpPrefix],
originated: &[IpPrefix],
limits: ExportLimits,
) -> (
mpsc::Receiver<Message>,
mpsc::UnboundedReceiver<SessionError>,
SessionGuard,
) {
) -> LimitedPeer {
let registry = SessionRegistry::new();
let rib = Arc::new(RibTable::new(4));
let local_rib = LocalRib::spawn();
@@ -1720,7 +1714,19 @@ mod tests {
});
handle.export_limits = Some(Arc::new(limits));
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
@@ -1764,17 +1770,17 @@ mod tests {
prefixes: allowing(&[nth_prefix(2), nth_prefix(11)]),
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(10), nth_prefix(11)],
limits,
)
.await;
assert_eq!(
announced(&mut outbound).await,
announced(&mut peer.outbound).await,
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,
@@ -1787,8 +1793,8 @@ mod tests {
prefixes: None,
max_prefixes: Some(3),
};
let (mut outbound, mut ending, _registration) = limited(&prefixes, &[], limits).await;
let why = tokio::time::timeout(Duration::from_secs(10), ending.recv())
let mut peer = limited(&prefixes, &[], limits).await;
let why = tokio::time::timeout(Duration::from_secs(10), peer.ending.recv())
.await
.expect("the session is asked to end")
.expect("by the exporter");
@@ -1802,7 +1808,7 @@ mod tests {
),
"{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
@@ -1814,8 +1820,8 @@ mod tests {
prefixes: None,
max_prefixes: Some(2),
};
let (mut outbound, mut ending, _registration) = limited(&[], &originated, limits).await;
let why = tokio::time::timeout(Duration::from_secs(10), ending.recv())
let mut peer = limited(&[], &originated, limits).await;
let why = tokio::time::timeout(Duration::from_secs(10), peer.ending.recv())
.await
.expect("the session is asked to end")
.expect("by the exporter");
@@ -1829,6 +1835,6 @@ mod tests {
),
"{why:?}"
);
assert_eq!(announced(&mut outbound).await.len(), 2);
assert_eq!(announced(&mut peer.outbound).await.len(), 2);
}
}
+13 -13
View File
@@ -157,9 +157,12 @@ impl Config {
Some(export) => parse_export(export)?,
None => ExportPolicy::default(),
});
let export_limits = parse_export_limits(proto.export_limits.as_ref())
let export_limits = match proto.export_limits.as_ref() {
Some(limits) => parse_export_limits(limits)
.map_err(ConfigError::ExportLimits)?
.map(Arc::new);
.map(Arc::new),
None => None,
};
let graceful_restart = match proto.graceful_restart.as_ref() {
Some(own) => GracefulRestartConfig::default().overridden_by(own)?,
None => GracefulRestartConfig::default(),
@@ -171,8 +174,8 @@ impl Config {
peer: name.clone(),
problem,
};
let peer =
parse_peer(peer, &export, &export_limits, graceful_restart).map_err(rejected)?;
let peer = parse_peer(peer, &export, export_limits.as_ref(), graceful_restart)
.map_err(rejected)?;
// RFC 9234 is about what networks are to each other.
if peer.role.is_some() && peer.remote_asn == proto.local_asn {
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
/// message is left out or says nothing.
/// Limits on what is announced; `None` for a block that says nothing,
/// which lifts them.
fn parse_export_limits(
limits: Option<&proto::ExportLimits>,
limits: &proto::ExportLimits,
) -> Result<Option<ExportLimits>, ImportProblem> {
let Some(limits) = limits else {
return Ok(None);
};
let limits = ExportLimits {
prefixes: if limits.prefixes.is_empty() {
None
@@ -352,7 +352,7 @@ fn parse_export_limits(
fn parse_peer(
peer: Peer,
export: &Arc<ExportPolicy>,
export_limits: &Option<Arc<ExportLimits>>,
export_limits: Option<&Arc<ExportLimits>>,
graceful_restart: GracefulRestartConfig,
) -> Result<PeerConfig, PeerProblem> {
let ep = peer.endpoint.as_ref().ok_or(PeerProblem::MissingEndpoint)?;
@@ -447,10 +447,10 @@ fn parse_peer(
None => Arc::clone(export),
},
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(Arc::new),
None => export_limits.clone(),
None => export_limits.cloned(),
},
graceful_restart: match peer.graceful_restart.as_ref() {
Some(own) => graceful_restart.overridden_by(own)?,
+27 -21
View File
@@ -188,6 +188,19 @@ pub struct SessionEnd {
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
/// side opened the connection.
pub async fn run_session(
@@ -214,28 +227,21 @@ pub async fn run_session(
};
let established = session.established.is_some();
let hold_downs = &session.server.hold_downs;
if matches!(result, Err(SessionError::PrefixLimitExceeded { .. })) {
// A peer that sends far more than it should will do so again on
// the next session; it is left alone for a while.
hold_downs.hold(
peer_addr.into(),
session.import.hold_down,
HeldDown::OverLimit,
);
PREFIX_LIMIT_EXCEEDED
.with_label_values(&[session.refusals.label()])
.inc();
// A peer that sent far more than it should, or that we were to send
// far more than we should, will be so again on the next session; it
// is left alone for a while, until it or we are put right.
let over_limit = match &result {
Err(SessionError::PrefixLimitExceeded { .. }) => {
Some((HeldDown::OverLimit, &*PREFIX_LIMIT_EXCEEDED))
}
if matches!(result, Err(SessionError::ExportLimitExceeded { .. })) {
// Whatever made us want to send too much will be there on the
// next session too. Until it is put right, the peer is told of
// nothing rather than of too much.
hold_downs.hold(
peer_addr.into(),
session.import.hold_down,
HeldDown::OverExportLimit,
);
EXPORT_LIMIT_EXCEEDED
Err(SessionError::ExportLimitExceeded { .. }) => {
Some((HeldDown::OverExportLimit, &*EXPORT_LIMIT_EXCEEDED))
}
_ => None,
};
if let Some((why, exceeded)) = over_limit {
hold_downs.hold(peer_addr.into(), session.import.hold_down, why);
exceeded
.with_label_values(&[session.refusals.label()])
.inc();
}
+3 -3
View File
@@ -18,7 +18,7 @@ use tracing::{info, warn};
use crate::collision::Direction;
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;
/// 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 {
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}")),
},
@@ -130,7 +130,7 @@ async fn dial_loop(peer: PeerConfig, server: Arc<ServerContext>, connect_retry:
};
let reason = match ended {
Ok(()) => "session closed at our request".to_string(),
Ok(()) => CLOSED_AT_OUR_REQUEST.to_string(),
Err(reason) => {
warn!(peer = %peer.name, endpoint = %peer.endpoint, err = %reason, "Session attempt ended");
reason
+7 -7
View File
@@ -296,10 +296,10 @@ impl ExportLimits {
.is_none_or(|prefixes| prefixes.allows(prefix))
}
/// Whether `routes` routes at a time are more than may be announced.
pub fn exceeded_by(&self, routes: usize) -> bool {
self.max_prefixes
.is_some_and(|limit| routes > limit as usize)
/// The limit that `routes` routes at a time would go over, if they
/// would.
pub fn exceeded_by(&self, routes: usize) -> Option<u32> {
self.max_prefixes.filter(|limit| routes > *limit as usize)
}
}
@@ -1391,10 +1391,10 @@ mod tests {
prefixes: None,
max_prefixes: Some(2),
};
assert!(!limits.exceeded_by(2));
assert!(limits.exceeded_by(3));
assert_eq!(limits.exceeded_by(2), None);
assert_eq!(limits.exceeded_by(3), Some(2));
assert!(limits.allows(&IpPrefix::try_from("203.0.113.0/24").unwrap()));
assert!(!ExportLimits::default().exceeded_by(usize::MAX));
assert_eq!(ExportLimits::default().exceeded_by(usize::MAX), None);
}
#[test]
+60 -61
View File
@@ -21,7 +21,7 @@ use bgp_rpki::Rpki;
use crate::address::{PeerAddress, socket_text};
use crate::advertise::spawn_advertiser;
use crate::collision::{CollisionTable, Direction};
use crate::config::Config;
use crate::config::{Config, PeerConfig};
use crate::connection::{PeerSettings, ServerContext, run_session};
use crate::dialer::spawn_dialers;
use crate::error::ServerError;
@@ -198,56 +198,8 @@ impl BgpServer {
"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 {
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`.
fn listen(addr: SocketAddr) -> io::Result<TcpListener> {
let socket = match addr {
@@ -480,23 +480,22 @@ async fn serve(
peer: PeerSettings,
_slot: ConnectionSlot,
) {
// Nobody dials a passive peer, so its state is said here.
let passive = peer.name.clone().filter(|_| peer.passive);
// Nobody dials a passive peer, so how its session ended is noted here.
let passive = if peer.passive {
peer.name.clone()
} else {
None
};
let registry = context.registry.clone();
let end = run_session(stream, peer_addr, Direction::Inbound, context, peer).await;
if let Err(e) = &end.result {
warn!(peer_addr = %socket_text(peer_addr), err = %e, "BGP session terminated with error");
}
if let Some(name) = passive {
registry.set_dial_state(
&name,
DialState::Passive {
last_error: Some(match end.result {
Ok(()) => "session closed at our request".to_string(),
Err(e) => e.to_string(),
}),
},
);
let state = DialState::Passive {
last_error: Some(end.reason()),
};
registry.set_dial_state(&name, state);
}
}
+42 -48
View File
@@ -327,6 +327,11 @@ fn as_path_text(path: &AsPathAttribute) -> String {
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
/// origins, of the three kinds of global administrator, by name; the rest
/// as their eight octets.
@@ -335,12 +340,7 @@ fn extended_community_text(community: &ExtendedCommunity) -> String {
let kind = match b[1] {
0x02 => "rt",
0x03 => "ro",
_ => {
return format!(
"0x{}",
b.iter().map(|o| format!("{o:02x}")).collect::<String>()
);
}
_ => return format!("0x{}", hex(&b)),
};
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]]);
@@ -348,17 +348,21 @@ fn extended_community_text(community: &ExtendedCommunity) -> String {
0x00 => format!("{kind} {}:{}", u16_at(2), u32_at(4)),
0x01 => format!("{kind} {}:{}", Ipv4Addr::from(u32_at(2)), u16_at(6)),
0x02 => format!("{kind} {}:{}", u32_at(2), u16_at(6)),
_ => format!(
"0x{}",
b.iter().map(|o| format!("{o:02x}")).collect::<String>()
),
_ => format!("0x{}", hex(&b)),
}
}
/// The name and value of an attribute the summary has no field for, or
/// `None` for one it has.
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::AsPath(_)
| PathAttribute::NextHop(_)
@@ -367,60 +371,50 @@ fn other_attribute(attribute: &PathAttribute) -> Option<OtherAttribute> {
| PathAttribute::Communities(_)
| PathAttribute::LargeCommunities(_)
| PathAttribute::MpReachNlri(_)
| PathAttribute::MpUnreachNlri(_) => return None,
PathAttribute::AtomicAggregate(_) => ("atomic aggregate".to_string(), "set".to_string()),
PathAttribute::Aggregator(a) => ("aggregator".to_string(), format!("AS{} {}", a.asn, a.ip)),
PathAttribute::ExtendedCommunities(e) => (
"extended communities".to_string(),
| PathAttribute::MpUnreachNlri(_) => None,
PathAttribute::AtomicAggregate(_) => other("atomic aggregate", "set".to_string()),
PathAttribute::Aggregator(a) => other("aggregator", format!("AS{} {}", a.asn, a.ip)),
PathAttribute::ExtendedCommunities(e) => other(
"extended communities",
list(
e.extended_communities
.iter()
.map(extended_community_text)
.collect::<Vec<_>>()
.join(", "),
.collect(),
", ",
),
PathAttribute::As4Path(p) => ("AS4_PATH".to_string(), as_path_text(p)),
PathAttribute::OnlyToCustomer(asn) => ("only to customer".to_string(), format!("AS{asn}")),
PathAttribute::OriginatorId(id) => ("originator ID".to_string(), id.to_string()),
PathAttribute::ClusterList(ids) => (
"cluster list".to_string(),
ids.iter()
.map(Ipv4Addr::to_string)
.collect::<Vec<_>>()
.join(" "),
),
PathAttribute::As4Path(p) => other("AS4_PATH", as_path_text(p)),
PathAttribute::OnlyToCustomer(asn) => other("only to customer", format!("AS{asn}")),
PathAttribute::OriginatorId(id) => other("originator ID", id.to_string()),
PathAttribute::ClusterList(ids) => other(
"cluster list",
list(ids.iter().map(Ipv4Addr::to_string).collect(), " "),
),
PathAttribute::UnknownPathAttribute {
flags,
type_code,
payload,
} => {
let kind = [
// "optional transitive, 2 octets: ab01"
let flags: Vec<&str> = [
(flags.optional(), "optional"),
(flags.transitive(), "transitive"),
(flags.partial(), "partial"),
]
.into_iter()
.filter_map(|(set, word)| set.then_some(word))
.collect::<Vec<_>>();
let octets = payload
.iter()
.map(|o| format!("{o:02x}"))
.collect::<String>();
(
format!("attribute {type_code}"),
format!(
"{}{} octets{}{octets}",
if kind.is_empty() {
String::new()
} else {
format!("{}, ", kind.join(" "))
},
payload.len(),
if payload.is_empty() { "" } else { ": " },
),
)
.collect();
let mut value = format!("{} octets", payload.len());
if !flags.is_empty() {
value = format!("{}, {value}", flags.join(" "));
}
if !payload.is_empty() {
value = format!("{value}: {}", hex(payload));
}
other(&format!("attribute {type_code}"), value)
}
}
};
Some(OtherAttribute { name, value })
}
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)`;
return '';
}
const [admin, fn, arg] = p;
const [admin, func, arg] = p;
if (admin !== L) return '';
if (fn === 0) return arg ? `not to AS${arg}` : 'to no other network';
if (fn === 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 (fn === 20) return `not to group ${arg}`;
if (fn === 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 === 0) return arg ? `not to AS${arg}` : 'to no other network';
if (func === 1) return `only to AS${arg} (and others named so)`;
if (func >= 11 && func <= 13) return `our AS ${TIMES[func - 10]} more on the path to ${arg ? `AS${arg}` : 'every network'}`;
if (func === 20) return `not to group ${arg}`;
if (func === 21) return `only to group ${arg} (and others named so)`;
if (func >= 31 && func <= 33) return `our AS ${TIMES[func - 30]} more on the path to group ${arg}`;
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
local AS and its customers originate.
At startup, Ferrix logs a warning for each neighbor to which export limits
apply but none are configured, and for each originated route that the
`prefixes` of the export limits do not permit.
At startup, Ferrix logs a warning that lists the neighbors to which export
limits apply but none are configured, and a warning for each originated
route that the `prefixes` of the export limits do not permit.
### Verifying Export Limits