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:
1 parent
2dfc00862b
commit
befc292215
9 files changed
+219
-214
No files matched your search
@@ -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
@@ -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)?,
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user