From 277b973f1aa7c4c2a72a6e58932bb9545c51d7f3 Mon Sep 17 00:00:00 2001 From: Rayhaan Jaufeerally Date: Wed, 7 Oct 2026 23:16:01 +0200 Subject: [PATCH] Status page: what moved, what is missing, and a dark room MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The attention strip's names lead to the peer's row, which lights up for a moment. The IPv4 and IPv6 sessions of one neighbour (NAME-v4, NAME-v6) are listed together, joined down the side, the name said once in full. A route count that moved by more than 2% between refreshes carries ▲ or ▼ for two minutes; a row whose state or count changed while the tab was in the background carries ● until it is clicked. A compact mode drops the next hop and session columns, and is remembered by the browser. Colours are variables, with a second set for prefers-color-scheme: dark. A new per-peer `expected_routes` says about how many routes a peer sends when all is well. The daemon only passes it to the API; the page lists an established peer with under half of it as needing attention, a minute after the session came up, with the expected count beside the actual one. An established session with next to no routes looked, until now, exactly like one that was fine. Co-Authored-By: Claude Fable 5.1 --- crates/integration-tests/src/lib.rs | 1 + crates/server/proto/config.proto | 9 + crates/server/src/config.rs | 22 ++ crates/server/src/server.rs | 1 + crates/server/src/session.rs | 2 + crates/web/src/api.rs | 6 + crates/web/static/statusz.html | 268 +++++++++++++++---- docs/content/docs/guides/peers.md | 29 ++ docs/content/docs/operations/monitoring.md | 21 +- docs/content/docs/reference/configuration.md | 1 + docs/content/docs/reference/http-api.md | 2 + 11 files changed, 305 insertions(+), 57 deletions(-) diff --git a/crates/integration-tests/src/lib.rs b/crates/integration-tests/src/lib.rs index 902aeab..35fa456 100644 --- a/crates/integration-tests/src/lib.rs +++ b/crates/integration-tests/src/lib.rs @@ -56,6 +56,7 @@ pub fn peer_config(name: &str, endpoint: impl Into, remote_asn: u32) - groups: Vec::new(), reflector_client: false, passive: false, + expected_routes: None, } } diff --git a/crates/server/proto/config.proto b/crates/server/proto/config.proto index 2efe0d1..e74ea34 100644 --- a/crates/server/proto/config.proto +++ b/crates/server/proto/config.proto @@ -384,6 +384,15 @@ message Peer { // new peer whose side is not ready yet, or one that is to open the // connections itself. bool passive = 16; + + // About how many routes the peer sends when all is well: a full table, + // a few hundred from an exchange peer, one from a customer. Nothing is + // decided by it. The status page compares the routes the peer has in + // the table with it, and shows an established session with fewer than + // half as needing attention: a peer that is up but has sent next to + // nothing looks, without this, exactly like one that is fine. Zero, the + // default, compares nothing. + uint32 expected_routes = 17; } // What is announced to a peer. diff --git a/crates/server/src/config.rs b/crates/server/src/config.rs index 3c27d7c..8fa6c9f 100644 --- a/crates/server/src/config.rs +++ b/crates/server/src/config.rs @@ -113,6 +113,9 @@ pub struct PeerConfig { pub reflector_client: bool, /// Do not dial the peer: wait for it to connect. pub passive: bool, + /// About how many routes the peer sends when all is well, for the + /// status page to compare with; nothing here acts on it. + pub expected_routes: Option, } impl Config { @@ -555,6 +558,7 @@ fn parse_peer(peer: Peer, defaults: &PeerDefaults) -> Result 0), }) } @@ -1494,6 +1498,24 @@ mod tests { )); } + /// What a peer is expected to send is kept for the status page; zero, + /// the default, is nothing to compare with. + #[test] + fn expected_routes_are_kept_when_given() { + let config = parse(&format!( + "{BASE}peers {{ name: \"a\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1 + expected_routes: 900000 }} + peers {{ name: \"b\" endpoint {{ address: \"192.0.2.3\" }} remote_asn: 2 }}" + )) + .unwrap(); + let expected: Vec> = config + .peers + .iter() + .map(|peer| peer.expected_routes) + .collect(); + assert_eq!(expected, [Some(900_000), None]); + } + /// A peer is dialled unless it is passive. #[test] fn a_peer_can_be_left_to_connect() { diff --git a/crates/server/src/server.rs b/crates/server/src/server.rs index fe9ffb2..2ad8656 100644 --- a/crates/server/src/server.rs +++ b/crates/server/src/server.rs @@ -127,6 +127,7 @@ impl BgpServer { remote_asn: p.remote_asn, role: p.role, passive: p.passive, + expected_routes: p.expected_routes, }) .collect(); // The client starts now: with luck the data is there before the diff --git a/crates/server/src/session.rs b/crates/server/src/session.rs index b184055..f199307 100644 --- a/crates/server/src/session.rs +++ b/crates/server/src/session.rs @@ -75,6 +75,8 @@ pub struct ConfiguredPeer { pub role: Option, /// Whether it is left to the peer to connect. pub passive: bool, + /// About how many routes it sends when all is well, if that was said. + pub expected_routes: Option, } /// Counts kept by a session as it goes, for whoever wants to look. diff --git a/crates/web/src/api.rs b/crates/web/src/api.rs index bef659a..c556b33 100644 --- a/crates/web/src/api.rs +++ b/crates/web/src/api.rs @@ -92,6 +92,9 @@ pub struct PeerView { /// How many of the peer's routes are held out of the table as invalid /// by the RPKI's data, to be taken if that changes. pub held_routes: Option, + /// About how many routes the peer sends when all is well, from the + /// configuration (`expected_routes`), for `routes` to be compared with. + pub expected_routes: Option, /// What the peer is to us, if configured: `provider`, `customer`, /// `peer`, `route server` or `route server client`. pub role: Option<&'static str>, @@ -157,6 +160,7 @@ impl From<&SessionHandle> for PeerView { routes: Some(s.counters.routes()), rejected_routes: Some(s.counters.rejected()), held_routes: Some(s.counters.held()), + expected_routes: None, role: s.role.map(PeerRole::as_str), role_confirmed: s.role.map(|_| s.role_confirmed), last_error: None, @@ -195,6 +199,7 @@ impl PeerView { routes: None, rejected_routes: None, held_routes: None, + expected_routes: peer.expected_routes, role: peer.role.map(PeerRole::as_str), role_confirmed: None, last_error, @@ -225,6 +230,7 @@ pub fn peer_views(registry: &SessionRegistry, restarts: &Restarts) -> Vec + Status for ferrix_bgp