Status page: what needs attention first, glyphs for what repeats
The page opened with every session in bold green, the normal state, and the brightest numbers on it were the rejection counts, which RPKI doing its job produces by the thousand. The two sessions that were actually down were in the same red, thirty rows down. Now: - A status line at the top says what needs attention, by name and with the reason: sessions down, peers restarting, the kernel behind or refusing routes, RPKI data missing or about to expire. When nothing does, it says so. A second line gives the normal state in grey. - Colour means one thing each. Red: failed, needs a hand. Amber: degraded, worth watching. Green, set small: a system doing its job. Grey: inactive and expected. Normal is black; "Established" is no longer green, and a rejection count no longer red. - Peers that need attention come first, then the others grouped by what they are to us, with a "problems only" switch. Numbers are right aligned with thousands separators; the established time and uptime are one cell; the session's properties are glyphs (v4, v6, GR, ENH, AP, 2B) with the meaning as a tooltip, as are the roles. - Received routes: the best route leads, marked ★; attributes without a column are glyphs (OTC, AA, AGG, AS4, OID, CL); validity is ✓ ✗ –. - Rejected routes: those alike (same prefix, reason and path) are one row with a count and the last time; each peer's reasons are a bar. - Communities with a configured name (community_names, new) are badges with that name, the number as the tooltip: "comrack", "SwissIX", "interconnection 2011" rather than 210036:100:2 and so on. A name for a pattern shows the open part after it. The daemon does nothing with the names but pass them to the page. - One lookup box at the top, which takes a peer's name too; a peer's name anywhere on the page leads to its routes; table headers and the status line stay in view while scrolling. - The host name comes from gethostname(2), not from running `hostname`, which a static binary on a bare machine could not find: comrack said "unknown host". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
23e8b4aadb
commit
1ce7eef27e
12 files changed
+515
-160
No files matched your search
Generated
+1
@@ -312,6 +312,7 @@ dependencies = [
|
|||||||
"bgp-rpki",
|
"bgp-rpki",
|
||||||
"bgp-server",
|
"bgp-server",
|
||||||
"futures",
|
"futures",
|
||||||
|
"nix",
|
||||||
"prometheus",
|
"prometheus",
|
||||||
"serde",
|
"serde",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|||||||
+14
-3
@@ -5,7 +5,7 @@ use std::sync::Arc;
|
|||||||
use bgp_server::config::Config;
|
use bgp_server::config::Config;
|
||||||
use bgp_server::proto::config::BgpConfig;
|
use bgp_server::proto::config::BgpConfig;
|
||||||
use bgp_server::server::{BgpServer, leave_for_restart, shut_down};
|
use bgp_server::server::{BgpServer, leave_for_restart, shut_down};
|
||||||
use bgp_web::{AppState, RouterInfo};
|
use bgp_web::{AppState, CommunityName, RouterInfo};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use eyre::{Context, Result};
|
use eyre::{Context, Result};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
@@ -80,12 +80,23 @@ async fn main() -> Result<ExitCode> {
|
|||||||
env!("CARGO_PKG_VERSION"),
|
env!("CARGO_PKG_VERSION"),
|
||||||
)
|
)
|
||||||
.with_config(args.config.to_string_lossy(), &config_string)
|
.with_config(args.config.to_string_lossy(), &config_string)
|
||||||
.with_control_communities(server.config().control.is_some()),
|
.with_control_communities(server.config().control.is_some())
|
||||||
|
.with_community_names(
|
||||||
|
server
|
||||||
|
.config()
|
||||||
|
.community_names
|
||||||
|
.iter()
|
||||||
|
.map(|(community, name)| CommunityName {
|
||||||
|
community: community.clone(),
|
||||||
|
name: name.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
registry: server.registry.clone(),
|
registry: server.registry.clone(),
|
||||||
local_rib: server.local_rib.clone(),
|
local_rib: server.local_rib.clone(),
|
||||||
rib: Arc::clone(&server.rib),
|
rib: Arc::clone(&server.rib),
|
||||||
refusals: server.refusals.clone(),
|
rejections: server.rejections.clone(),
|
||||||
rpki: server.rpki.clone(),
|
rpki: server.rpki.clone(),
|
||||||
kernel: server.kernel.clone(),
|
kernel: server.kernel.clone(),
|
||||||
restarts: Arc::clone(&server.restarts),
|
restarts: Arc::clone(&server.restarts),
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ impl TestServer {
|
|||||||
kernel: None,
|
kernel: None,
|
||||||
export: Default::default(),
|
export: Default::default(),
|
||||||
export_limits: None,
|
export_limits: None,
|
||||||
|
community_names: Vec::new(),
|
||||||
control: None,
|
control: None,
|
||||||
graceful_restart: Default::default(),
|
graceful_restart: Default::default(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -120,6 +120,23 @@ message BgpConfig {
|
|||||||
// passed on to customers only; these keep it from going further even
|
// passed on to customers only; these keep it from going further even
|
||||||
// if a route is not marked as it should be.
|
// if a route is not marked as it should be.
|
||||||
ExportLimits export_limits = 16;
|
ExportLimits export_limits = 16;
|
||||||
|
|
||||||
|
// Names for communities, for the status page to show in place of the
|
||||||
|
// numbers: what the marks on routes mean to a reader. A name for one
|
||||||
|
// community ("64601:100:2" is "comrack"), or for a kind of them, with
|
||||||
|
// `*` for the part left open ("64601:102:*" is "interconnection"), which
|
||||||
|
// is shown with the open part after the name. The daemon itself does
|
||||||
|
// nothing with them.
|
||||||
|
repeated CommunityName community_names = 17;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A name for a community, or for a kind of them.
|
||||||
|
message CommunityName {
|
||||||
|
// The community, standard ("64601:100") or large ("64601:100:2"), or a
|
||||||
|
// pattern of them with `*` for a part left open ("64601:102:*").
|
||||||
|
string community = 1;
|
||||||
|
// What it is shown as.
|
||||||
|
string name = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limits on what is announced to a peer, which hold whatever `export`
|
// Limits on what is announced to a peer, which hold whatever `export`
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ pub struct Config {
|
|||||||
/// The communities by which routes say who is told of them, if they
|
/// The communities by which routes say who is told of them, if they
|
||||||
/// are honoured.
|
/// are honoured.
|
||||||
pub control: Option<Control>,
|
pub control: Option<Control>,
|
||||||
|
/// Names for communities, as (community or pattern, name), for the
|
||||||
|
/// status page.
|
||||||
|
pub community_names: Vec<(String, String)>,
|
||||||
/// Graceful restart for peers that have no setting of their own; and
|
/// Graceful restart for peers that have no setting of their own; and
|
||||||
/// the restart time this daemon gives, which decides how long it waits
|
/// the restart time this daemon gives, which decides how long it waits
|
||||||
/// after it starts for its peers to send their routes.
|
/// after it starts for its peers to send their routes.
|
||||||
@@ -158,6 +161,7 @@ impl Config {
|
|||||||
proto.local_asn,
|
proto.local_asn,
|
||||||
)?;
|
)?;
|
||||||
let originate = parse_originate(&proto.originate)?;
|
let originate = parse_originate(&proto.originate)?;
|
||||||
|
let community_names = parse_community_names(&proto.community_names)?;
|
||||||
let ours = Ours {
|
let ours = Ours {
|
||||||
local_asn: proto.local_asn,
|
local_asn: proto.local_asn,
|
||||||
router_id,
|
router_id,
|
||||||
@@ -202,11 +206,36 @@ impl Config {
|
|||||||
control: proto.control_communities.then_some(Control {
|
control: proto.control_communities.then_some(Control {
|
||||||
local_asn: proto.local_asn,
|
local_asn: proto.local_asn,
|
||||||
}),
|
}),
|
||||||
|
community_names,
|
||||||
graceful_restart: defaults.graceful_restart,
|
graceful_restart: defaults.graceful_restart,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The names for communities, each checked to name a community or a
|
||||||
|
/// pattern of them.
|
||||||
|
fn parse_community_names(
|
||||||
|
names: &[proto::CommunityName],
|
||||||
|
) -> Result<Vec<(String, String)>, ConfigError> {
|
||||||
|
names
|
||||||
|
.iter()
|
||||||
|
.map(|entry| {
|
||||||
|
Patterns::parse(std::slice::from_ref(&entry.community)).map_err(|problem| {
|
||||||
|
ConfigError::CommunityName {
|
||||||
|
community: entry.community.clone(),
|
||||||
|
problem,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
if entry.name.trim().is_empty() {
|
||||||
|
return Err(ConfigError::CommunityNameEmpty {
|
||||||
|
community: entry.community.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok((entry.community.clone(), entry.name.clone()))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Where BGP connections are taken: as configured, or on every address.
|
/// Where BGP connections are taken: as configured, or on every address.
|
||||||
fn parse_listen_endpoints(
|
fn parse_listen_endpoints(
|
||||||
endpoints: &[proto::TcpEndpoint],
|
endpoints: &[proto::TcpEndpoint],
|
||||||
|
|||||||
@@ -213,6 +213,13 @@ pub enum ConfigError {
|
|||||||
Export(#[from] ExportProblem),
|
Export(#[from] ExportProblem),
|
||||||
#[error("export_limits: {0}")]
|
#[error("export_limits: {0}")]
|
||||||
ExportLimits(ImportProblem),
|
ExportLimits(ImportProblem),
|
||||||
|
#[error("community_names {community:?}: {problem}")]
|
||||||
|
CommunityName {
|
||||||
|
community: String,
|
||||||
|
problem: crate::marks::MarksProblem,
|
||||||
|
},
|
||||||
|
#[error("community_names {community:?}: the name is empty")]
|
||||||
|
CommunityNameEmpty { community: String },
|
||||||
#[error(
|
#[error(
|
||||||
"accept_unconfigured_peers is set and export.learned_routes passes learned routes on: \
|
"accept_unconfigured_peers is set and export.learned_routes passes learned routes on: \
|
||||||
a peer that is not configured has no role, so the routes it sends would go to every \
|
a peer that is not configured has no role, so the routes it sends would go to every \
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ serde.workspace = true
|
|||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tokio-stream.workspace = true
|
tokio-stream.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
nix = { workspace = true, features = ["hostname"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
@@ -25,7 +25,7 @@ use bgp_server::rpki::PathCheck;
|
|||||||
use bgp_server::session::{ConfiguredPeer, DialState, PeerId, SessionHandle, SessionRegistry};
|
use bgp_server::session::{ConfiguredPeer, DialState, PeerId, SessionHandle, SessionRegistry};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::{AppState, CommunityName};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Views
|
// Views
|
||||||
@@ -54,6 +54,9 @@ pub struct StatusView {
|
|||||||
/// Whether large communities of our AS ask for things here
|
/// Whether large communities of our AS ask for things here
|
||||||
/// (`control_communities`).
|
/// (`control_communities`).
|
||||||
pub control_communities: bool,
|
pub control_communities: bool,
|
||||||
|
/// Names for communities, as configured, for showing in place of the
|
||||||
|
/// numbers.
|
||||||
|
pub community_names: Vec<CommunityName>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -600,6 +603,7 @@ pub async fn status(State(state): State<AppState>) -> Result<Json<StatusView>, U
|
|||||||
received_routes: state.rib.len(),
|
received_routes: state.rib.len(),
|
||||||
received_prefixes: state.rib.prefix_count(),
|
received_prefixes: state.rib.prefix_count(),
|
||||||
control_communities: state.info.control_communities,
|
control_communities: state.info.control_communities,
|
||||||
|
community_names: state.info.community_names.clone(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-6
@@ -23,6 +23,7 @@ use bgp_rpki::Rpki;
|
|||||||
use bgp_server::rejections::RejectionLog;
|
use bgp_server::rejections::RejectionLog;
|
||||||
use bgp_server::restart::Restarts;
|
use bgp_server::restart::Restarts;
|
||||||
use bgp_server::session::SessionRegistry;
|
use bgp_server::session::SessionRegistry;
|
||||||
|
use serde::Serialize;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
@@ -48,6 +49,18 @@ pub struct RouterInfo {
|
|||||||
/// Whether large communities of our AS ask for things here
|
/// Whether large communities of our AS ask for things here
|
||||||
/// (`control_communities`), so that the status page can say what.
|
/// (`control_communities`), so that the status page can say what.
|
||||||
pub control_communities: bool,
|
pub control_communities: bool,
|
||||||
|
/// Names for communities, as configured (`community_names`), for the
|
||||||
|
/// status page to show in place of the numbers.
|
||||||
|
pub community_names: Vec<CommunityName>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A name for a community, or for a kind of them.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct CommunityName {
|
||||||
|
/// The community, or a pattern of them with `*` for a part left open:
|
||||||
|
/// `64601:100:2`, `64601:102:*`.
|
||||||
|
pub community: String,
|
||||||
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RouterInfo {
|
impl RouterInfo {
|
||||||
@@ -61,9 +74,16 @@ impl RouterInfo {
|
|||||||
hostname: hostname(),
|
hostname: hostname(),
|
||||||
config: None,
|
config: None,
|
||||||
control_communities: false,
|
control_communities: false,
|
||||||
|
community_names: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Records the names configured for communities.
|
||||||
|
pub fn with_community_names(mut self, names: Vec<CommunityName>) -> Self {
|
||||||
|
self.community_names = names;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Records whether control communities are acted on.
|
/// Records whether control communities are acted on.
|
||||||
pub fn with_control_communities(mut self, on: bool) -> Self {
|
pub fn with_control_communities(mut self, on: bool) -> Self {
|
||||||
self.control_communities = on;
|
self.control_communities = on;
|
||||||
@@ -80,13 +100,10 @@ impl RouterInfo {
|
|||||||
/// Best-effort host name without pulling in a dependency: ask the `hostname`
|
/// Best-effort host name without pulling in a dependency: ask the `hostname`
|
||||||
/// utility, which exists on every platform we run on.
|
/// utility, which exists on every platform we run on.
|
||||||
fn hostname() -> String {
|
fn hostname() -> String {
|
||||||
std::process::Command::new("hostname")
|
nix::unistd::gethostname()
|
||||||
.output()
|
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|o| o.status.success())
|
.and_then(|name| name.into_string().ok())
|
||||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
.filter(|name| !name.is_empty())
|
||||||
.map(|s| s.trim().to_string())
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.unwrap_or_else(|| "unknown host".into())
|
.unwrap_or_else(|| "unknown host".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+351
-137
@@ -12,45 +12,77 @@
|
|||||||
framework, no external resources. Everything shown comes from /api/*,
|
framework, no external resources. Everything shown comes from /api/*,
|
||||||
except the names of AS numbers: pointing at one asks RIPEstat for its
|
except the names of AS numbers: pointing at one asks RIPEstat for its
|
||||||
holder, from the browser (the daemon itself looks nothing up).
|
holder, from the browser (the daemon itself looks nothing up).
|
||||||
|
|
||||||
|
How colour is used, and not used: what is normal is black on white, and
|
||||||
|
draws no attention. Red is for what has failed and needs a hand (a
|
||||||
|
session down, the kernel refusing routes). Amber is for what is degraded
|
||||||
|
or wants watching (routes held or stale, a peer restarting, RPKI data
|
||||||
|
about to expire). Green is for a system that is actively doing its job
|
||||||
|
(RPKI synchronised, kernel in step), set small. Grey is for what is
|
||||||
|
inactive or expected (a passive peer waiting, a rejection count). What
|
||||||
|
repeats on every row is a glyph with a tooltip, not a sentence.
|
||||||
-->
|
-->
|
||||||
<style>
|
<style>
|
||||||
body { font-family: sans-serif; font-size: 13px; color: #000; background: #fff; margin: 8px; }
|
body { font-family: sans-serif; font-size: 13px; color: #000; background: #fff; margin: 8px; }
|
||||||
h1 { clear: both; width: 100%; text-align: center; font-size: 120%; background: #eef; margin: 14px 0 6px; padding: 2px 0; }
|
h1 { clear: both; width: 100%; text-align: center; font-size: 120%; background: #eef; margin: 14px 0 6px; padding: 2px 0; }
|
||||||
h1.top { font-size: 150%; margin-top: 0; }
|
h1.top { font-size: 150%; margin-top: 0; }
|
||||||
.lefthand { float: left; width: 70%; }
|
.lefthand { float: left; width: 60%; }
|
||||||
.righthand { text-align: right; }
|
.righthand { text-align: right; }
|
||||||
a { color: #00c; } a:visited { color: #551a8b; }
|
a { color: #00c; } a:visited { color: #551a8b; }
|
||||||
table.data { border-collapse: collapse; margin: 4px 0; }
|
table.data { border-collapse: collapse; margin: 4px 0; }
|
||||||
table.data th, table.data td { border: 1px solid #99a; padding: 2px 6px; text-align: left; vertical-align: top; }
|
table.data th, table.data td { border: 1px solid #99a; padding: 2px 6px; text-align: left; vertical-align: top; }
|
||||||
table.data th { background: #dde; font-weight: bold; }
|
table.data th { background: #dde; font-weight: bold; position: sticky; top: 0; z-index: 2; }
|
||||||
table.data > tbody > tr:hover > td { background: #f3f3ff; }
|
table.data > tbody > tr:hover > td { background: #f3f3ff; }
|
||||||
|
table.data td.num, table.data th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||||
|
table.data tr.group > td { background: #eef; font-weight: bold; padding: 3px 6px; }
|
||||||
|
table.data tr.best > td { background: #f4faf4; }
|
||||||
|
table.data tr.dim td { color: #666; }
|
||||||
.asn { cursor: help; }
|
.asn { cursor: help; }
|
||||||
.asn:hover { background: #dde; }
|
.asn:hover { background: #dde; }
|
||||||
#asn-tip { position: fixed; z-index: 10; max-width: 36em; background: #ffffe8; border: 1px solid #99a; padding: 2px 6px; font-size: 12px; pointer-events: none; }
|
#asn-tip { position: fixed; z-index: 10; max-width: 36em; background: #ffffe8; border: 1px solid #99a; padding: 2px 6px; font-size: 12px; pointer-events: none; }
|
||||||
.mono, table.data td.mono { font-family: monospace; }
|
.mono, table.data td.mono { font-family: monospace; }
|
||||||
.ok { color: #080; font-weight: bold; } .bad { color: #c00; font-weight: bold; } .warn { color: #a60; font-weight: bold; }
|
/* The colour grammar: see the note at the top. */
|
||||||
|
.red { color: #c00; font-weight: bold; }
|
||||||
|
.amber { color: #a60; font-weight: bold; }
|
||||||
|
.green { color: #080; }
|
||||||
.dim { color: #666; }
|
.dim { color: #666; }
|
||||||
.nowrap { white-space: nowrap; }
|
.nowrap { white-space: nowrap; }
|
||||||
table.data td.path { min-width: 12em; }
|
/* Glyphs: small bordered badges for what repeats on every row. */
|
||||||
#event-log { font-family: monospace; font-size: 12px; max-height: 22em; overflow-y: auto; }
|
.g { display: inline-block; font-size: 10.5px; line-height: 1.2; padding: 0 4px; margin: 0 2px 1px 0; border: 1px solid #aab; border-radius: 3px; background: #f6f6fb; color: #223; font-family: sans-serif; font-weight: normal; white-space: nowrap; vertical-align: middle; cursor: help; }
|
||||||
#event-log div { white-space: pre-wrap; }
|
.g.red { border-color: #c00; background: #fff0f0; color: #c00; font-weight: bold; }
|
||||||
|
.g.amber { border-color: #a60; background: #fff8e8; color: #a60; }
|
||||||
|
.g.green { border-color: #080; background: #f0fff0; color: #080; }
|
||||||
|
.g.dim { border-color: #ccd; background: #fafafa; color: #666; }
|
||||||
|
.g.mark { background: #eef4ff; border-color: #88a; }
|
||||||
|
.g.ctl { background: #fff4e8; border-color: #c96; }
|
||||||
|
.g.wk { background: #f0f0f0; border-color: #999; font-weight: bold; }
|
||||||
/* Communities: one line per AS that defines them, each kept whole. */
|
/* Communities: one line per AS that defines them, each kept whole. */
|
||||||
.comm { white-space: nowrap; margin-right: 0.7em; }
|
.comm { white-space: nowrap; margin-right: 0.7em; }
|
||||||
.comm[title] { border-bottom: 1px dotted #666; cursor: help; }
|
.comm[title] { border-bottom: 1px dotted #666; cursor: help; }
|
||||||
/* A line that wraps carries on indented, so each AS's line stays apart. */
|
|
||||||
.comms > div { padding-left: 2em; text-indent: -2em; margin-bottom: 2px; }
|
.comms > div { padding-left: 2em; text-indent: -2em; margin-bottom: 2px; }
|
||||||
.comms { min-width: 30em; }
|
.comms { min-width: 26em; }
|
||||||
table.kv th { background: #eef; font-weight: normal; }
|
table.kv th { background: #eef; font-weight: normal; position: static; }
|
||||||
table.data tr.dim td { color: #666; }
|
table.data td.path { min-width: 12em; }
|
||||||
|
table.data td.name { white-space: nowrap; }
|
||||||
|
/* The strip: what needs attention, if anything. */
|
||||||
|
#strip { position: sticky; top: 0; z-index: 3; background: #fff; border: 1px solid #99a; padding: 4px 8px; margin: 6px 0; }
|
||||||
|
#strip .item { display: inline-block; margin-right: 1.5em; white-space: nowrap; }
|
||||||
|
/* The breakdown of a peer's rejections, by reason. */
|
||||||
|
.bar { display: inline-flex; width: 16em; height: 10px; border: 1px solid #99a; vertical-align: middle; margin: 0 6px; background: #fff; }
|
||||||
|
.bar span { display: block; height: 100%; }
|
||||||
details { clear: both; margin: 8px 0 0; }
|
details { clear: both; margin: 8px 0 0; }
|
||||||
summary { cursor: pointer; color: #00c; }
|
summary { cursor: pointer; color: #00c; }
|
||||||
summary:hover { text-decoration: underline; }
|
summary:hover { text-decoration: underline; }
|
||||||
#config-text { border: 1px solid #99a; padding: 4px 6px; margin: 4px 0; font-size: 12px; overflow-x: auto; }
|
#config-text { border: 1px solid #99a; padding: 4px 6px; margin: 4px 0; font-size: 12px; overflow-x: auto; }
|
||||||
address { font-size: smaller; margin-top: 2em; color: #444; }
|
address { font-size: smaller; margin-top: 2em; color: #444; }
|
||||||
form.lookup { margin: 4px 0; }
|
form.lookup { margin: 4px 0; }
|
||||||
|
form.lookup input[type=text] { font-family: monospace; }
|
||||||
.pager { margin: 4px 0; }
|
.pager { margin: 4px 0; }
|
||||||
.pager a, .pager span { margin-right: 1em; }
|
.pager a, .pager span { margin-right: 1em; }
|
||||||
form.lookup input[type=text] { font-family: monospace; }
|
.toolbar { margin: 4px 0; }
|
||||||
|
.toolbar label { margin-right: 1em; }
|
||||||
|
#event-log { font-family: monospace; font-size: 12px; max-height: 22em; overflow-y: auto; }
|
||||||
|
#event-log div { white-space: pre-wrap; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -59,43 +91,19 @@ form.lookup input[type=text] { font-family: monospace; }
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="lefthand">
|
<div class="lefthand">
|
||||||
Started: <span id="s-started">...</span> (up <span id="s-uptime">...</span>)<br>
|
<b id="s-hostname">...</b>: router ID <b id="s-router-id">...</b>, AS <b id="s-asn">...</b>, Ferrix <span id="s-version">...</span><br>
|
||||||
Version <span id="s-version">...</span><br>
|
Started <span id="s-started">...</span>, up <span id="s-uptime">...</span><br>
|
||||||
Router ID <b id="s-router-id">...</b>, local AS <b id="s-asn">...</b><br>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="righthand">
|
<div class="righthand">
|
||||||
Running on <span id="s-hostname">...</span><br>
|
|
||||||
View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <a href="/api/peers">peers json</a>, <a href="/api/routes/local">originated routes json</a>, <a href="/api/routes/received">received routes json</a>, <a href="/api/routes/rejected">rejected routes json</a>, <a href="/api/rpki">rpki json</a>, <a href="/api/kernel">kernel json</a><br>
|
|
||||||
<span id="refresh-ctl"></span><br>
|
<span id="refresh-ctl"></span><br>
|
||||||
|
JSON: <a href="/api/status">status</a>, <a href="/api/peers">peers</a>, <a href="/api/routes/local">originated</a>, <a href="/api/routes/received">received</a>, <a href="/api/routes/rejected">rejected</a>, <a href="/api/rpki">rpki</a>, <a href="/api/kernel">kernel</a>; <a href="/metricz">metrics</a><br>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details id="config-zippy" hidden>
|
<div id="strip" style="clear: both">...</div>
|
||||||
<summary>Configuration <span class="dim" id="config-path"></span></summary>
|
|
||||||
<pre id="config-text"></pre>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<h1 id="peers">Peers (<span id="peers-count">...</span>)</h1>
|
|
||||||
<table class="data">
|
|
||||||
<thead><tr><th>name</th><th>peer</th><th>AS</th><th>state</th><th>opened by</th><th>local address (next hop)</th><th>established</th><th>uptime</th><th>4-byte AS</th><th>address families</th><th>routes</th><th title="Announcements rejected by the import policy since the daemon started. A route that is announced again and rejected again counts again.">rejected announcements</th><th>detail</th></tr></thead>
|
|
||||||
<tbody id="peers-body"></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h1 id="rpki">Route origin and path validation (RPKI)</h1>
|
|
||||||
<div id="rpki-body">...</div>
|
|
||||||
|
|
||||||
<h1 id="local">Originated routes (<span id="local-count">...</span>)</h1>
|
|
||||||
<table class="data">
|
|
||||||
<thead><tr><th>prefix</th><th>origin</th><th>AS path</th><th>MED</th><th>communities</th><th>large communities</th><th>since</th></tr></thead>
|
|
||||||
<tbody id="local-body"></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h1 id="kernel">Kernel routes</h1>
|
|
||||||
<div id="kernel-body">...</div>
|
|
||||||
|
|
||||||
<h1 id="received">Received routes (<span id="received-count">...</span>)</h1>
|
|
||||||
<form class="lookup" id="lookup">
|
<form class="lookup" id="lookup">
|
||||||
Look up <input type="text" id="lookup-q" size="30" placeholder="193.0.0.0/21 or 193.0.0.1" title="A prefix, or an address to find every prefix that covers it">
|
Look up <input type="text" id="lookup-q" size="30" placeholder="prefix, address, or peer" title="A prefix, an address to find every prefix that covers it, or a peer's name">
|
||||||
<select id="lookup-mode" title="What to find for a prefix">
|
<select id="lookup-mode" title="What to find for a prefix">
|
||||||
<option value="exact">routes for this prefix or address</option>
|
<option value="exact">routes for this prefix or address</option>
|
||||||
<option value="within">this prefix and everything inside it</option>
|
<option value="within">this prefix and everything inside it</option>
|
||||||
@@ -106,17 +114,46 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
<a href="#" id="lookup-clear">show all</a>
|
<a href="#" id="lookup-clear">show all</a>
|
||||||
<span id="lookup-note"></span>
|
<span id="lookup-note"></span>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<details id="config-zippy" hidden>
|
||||||
|
<summary>Configuration <span class="dim" id="config-path"></span></summary>
|
||||||
|
<pre id="config-text"></pre>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<h1 id="peers">Peers (<span id="peers-count">...</span>)</h1>
|
||||||
|
<div class="toolbar">
|
||||||
|
<label><input type="checkbox" id="peers-problems"> problems only</label>
|
||||||
|
<span class="dim">Glyphs: <span class="g">v4</span><span class="g">v6</span> address families · <span class="g">GR</span> graceful restart · <span class="g">ENH</span> IPv4 with IPv6 next hops · <span class="g">AP</span> ADD-PATH · <span class="g amber">2B</span> two-octet AS numbers only · <span class="g">↑</span> provider <span class="g">↓</span> customer <span class="g">↔</span> peer <span class="g">RS</span> route server <span class="g">RSC</span> its client <span class="g">iBGP</span> our AS. Point at one for its meaning; click a peer's name to look at its routes.</span>
|
||||||
|
</div>
|
||||||
|
<table class="data">
|
||||||
|
<thead><tr><th>name</th><th>peer</th><th>state</th><th>up for</th><th>our address (next hop)</th><th>session</th><th class="num">routes</th><th class="num" title="Announcements rejected by the import policy since the daemon started, and routes held out of the table as invalid or unreachable now">rejected · held</th><th>detail</th></tr></thead>
|
||||||
|
<tbody id="peers-body"></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h1 id="rpki">Route origin and path validation (RPKI)</h1>
|
||||||
|
<div id="rpki-body">...</div>
|
||||||
|
|
||||||
|
<h1 id="local">Originated routes (<span id="local-count">...</span>)</h1>
|
||||||
|
<table class="data">
|
||||||
|
<thead><tr><th>prefix</th><th>origin</th><th>AS path</th><th class="num">MED</th><th>communities</th><th>large communities</th><th>since</th></tr></thead>
|
||||||
|
<tbody id="local-body"></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h1 id="kernel">Kernel routes</h1>
|
||||||
|
<div id="kernel-body">...</div>
|
||||||
|
|
||||||
|
<h1 id="received">Received routes (<span id="received-count">...</span>)</h1>
|
||||||
<div class="pager" id="received-pager"></div>
|
<div class="pager" id="received-pager"></div>
|
||||||
<table class="data">
|
<table class="data">
|
||||||
<thead><tr><th>prefix</th><th>from peer</th><th>AS path</th><th>next hop</th><th>origin</th><th>MED</th><th>local pref</th><th>communities</th><th>large communities</th><th>other attributes</th><th>RPKI</th><th>ASPA</th><th>best</th></tr></thead>
|
<thead><tr><th></th><th>prefix</th><th>from peer</th><th>AS path</th><th>next hop</th><th>origin</th><th class="num">MED</th><th class="num">local pref</th><th>communities</th><th>large communities</th><th>attrs</th><th title="Route origin validation (RPKI) and AS path verification (ASPA)">RPKI · ASPA</th></tr></thead>
|
||||||
<tbody id="received-body"></tbody>
|
<tbody id="received-body"></tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="pager" id="received-pager-below"></div>
|
<div class="pager" id="received-pager-below"></div>
|
||||||
|
|
||||||
<h1 id="rejected">Rejected routes (<span id="rejected-count">...</span>)</h1>
|
<h1 id="rejected">Rejected routes (<span id="rejected-count">...</span>)</h1>
|
||||||
<div class="dim">Routes that peers announced and their import policies did not take. The counts are of announcements since the daemon started: a route that a peer announces again, and that is rejected again, counts again. The latest rejected routes of each peer are kept.</div>
|
<div class="dim">Routes that peers announced and their import policies did not take. The counts are of announcements since the daemon started: a route announced again and rejected again counts again. Of each peer's latest rejections, those alike are one line, with how many and when the last was.</div>
|
||||||
<table class="data">
|
<table class="data">
|
||||||
<thead><tr><th>peer</th><th>prefix</th><th>AS path</th><th>reason</th><th>when</th><th>next hop</th><th>origin</th><th>MED</th><th>local pref</th><th>communities</th><th>large communities</th><th>other attributes</th></tr></thead>
|
<thead><tr><th>peer</th><th>prefix</th><th>AS path</th><th>reason</th><th class="num">times</th><th>last</th><th>next hop</th><th>origin</th><th>communities</th><th>large communities</th><th>attrs</th></tr></thead>
|
||||||
<tbody id="rejected-body"></tbody>
|
<tbody id="rejected-body"></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -138,7 +175,10 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
const esc = s => String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
const esc = s => String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||||
const state = { status: null, peers: [], local: [], received: [], receivedPage: {}, rejected: [], rpki: null, kernel: null };
|
const state = { status: null, peers: [], local: [], received: [], receivedPage: {}, rejected: [], rpki: null, kernel: null };
|
||||||
const DASH = '<span class="dim">-</span>';
|
const DASH = '<span class="dim">–</span>';
|
||||||
|
const fmtCount = n => Number(n).toLocaleString('en-US');
|
||||||
|
// A glyph: a small badge with its meaning as the tooltip.
|
||||||
|
const glyph = (text, title, cls = '') => `<span class="g ${cls}" title="${esc(title)}">${esc(text)}</span>`;
|
||||||
|
|
||||||
// ---- helpers -----------------------------------------------------------
|
// ---- helpers -----------------------------------------------------------
|
||||||
const fmtDur = s => {
|
const fmtDur = s => {
|
||||||
@@ -160,8 +200,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
document.querySelectorAll('[data-until]').forEach(e => { const v = fmtDur(e.dataset.until - t); if (e.textContent !== v) e.textContent = v; });
|
document.querySelectorAll('[data-until]').forEach(e => { const v = fmtDur(e.dataset.until - t); if (e.textContent !== v) e.textContent = v; });
|
||||||
if (state.status) $('s-uptime').textContent = fmtDur(t - state.status.started_at_unix);
|
if (state.status) $('s-uptime').textContent = fmtDur(t - state.status.started_at_unix);
|
||||||
}
|
}
|
||||||
const fmtTime = unix => new Date(unix * 1000).toLocaleTimeString();
|
const fmtWhen = unix => new Date(unix * 1000).toLocaleString();
|
||||||
const fmtCount = n => Number(n).toLocaleString('en-US');
|
|
||||||
|
|
||||||
// What a community of ours asks for where control communities are on
|
// What a community of ours asks for where control communities are on
|
||||||
// (control_communities), or ''. They are acted on where a route leaves
|
// (control_communities), or ''. They are acted on where a route leaves
|
||||||
@@ -188,10 +227,30 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The configured name for a community (community_names): an exact one
|
||||||
|
// first, then a pattern, whose open part is shown after the name.
|
||||||
|
function communityName(c) {
|
||||||
|
// An older daemon's status has no names.
|
||||||
|
const names = (state.status && state.status.community_names) || [];
|
||||||
|
const parts = c.split(':');
|
||||||
|
let fallback = null;
|
||||||
|
for (const entry of names) {
|
||||||
|
const pattern = entry.community.split(':');
|
||||||
|
if (pattern.length !== parts.length) continue;
|
||||||
|
if (entry.community === c) return entry.name;
|
||||||
|
if (!fallback && pattern.every((p, i) => p === '*' || p === parts[i])) {
|
||||||
|
const open = parts.filter((_, i) => pattern[i] === '*').join(':');
|
||||||
|
fallback = `${entry.name} ${open}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
// Communities, one line for each AS that defines them, in the order the
|
// Communities, one line for each AS that defines them, in the order the
|
||||||
// route has them; each whole, so it can be copied. Pointing at a line
|
// route has them. Well-known ones by name; ours that ask for something
|
||||||
// names its AS. Well-known ones by name, and ours that ask for something
|
// with what they ask as a tooltip; those with a configured name as a
|
||||||
// with what they ask.
|
// badge with that name, the number as the tooltip. Pointing at a line
|
||||||
|
// names its AS.
|
||||||
function commList(list, large) {
|
function commList(list, large) {
|
||||||
if (!list.length) return DASH;
|
if (!list.length) return DASH;
|
||||||
const owners = new Map();
|
const owners = new Map();
|
||||||
@@ -203,9 +262,11 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
const local = state.status ? String(state.status.local_asn) : null;
|
const local = state.status ? String(state.status.local_asn) : null;
|
||||||
const lines = [...owners].map(([owner, cs]) => {
|
const lines = [...owners].map(([owner, cs]) => {
|
||||||
const items = cs.map(c => {
|
const items = cs.map(c => {
|
||||||
const name = !large && WELL_KNOWN[c];
|
const wk = !large && WELL_KNOWN[c];
|
||||||
if (name) return `<span class="comm" title="${esc(c)}">${name}</span>`;
|
if (wk) return glyph(wk, c, 'wk');
|
||||||
|
const name = communityName(c);
|
||||||
const meaning = controlMeaning(c, large);
|
const meaning = controlMeaning(c, large);
|
||||||
|
if (name) return glyph(name, meaning ? `${c}: ${meaning}` : c, meaning ? 'ctl' : 'mark');
|
||||||
return meaning
|
return meaning
|
||||||
? `<span class="comm" title="${esc(`${meaning}: a control community, acted on where the route leaves AS${state.status.local_asn}`)}">${esc(c)}</span>`
|
? `<span class="comm" title="${esc(`${meaning}: a control community, acted on where the route leaves AS${state.status.local_asn}`)}">${esc(c)}</span>`
|
||||||
: `<span class="comm">${esc(c)}</span>`;
|
: `<span class="comm">${esc(c)}</span>`;
|
||||||
@@ -217,9 +278,21 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
return `<div class="comms mono">${lines.join('')}</div>`;
|
return `<div class="comms mono">${lines.join('')}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The attributes that have no column of their own.
|
// The attributes that have no column of their own, as glyphs.
|
||||||
|
const OTHER_GLYPHS = {
|
||||||
|
'atomic aggregate': ['AA', 'ATOMIC_AGGREGATE: the route is an aggregate that lost information'],
|
||||||
|
'aggregator': ['AGG', 'AGGREGATOR: the AS and router that made the aggregate'],
|
||||||
|
'extended communities': ['EXT', 'Extended communities'],
|
||||||
|
'AS4_PATH': ['AS4', 'AS4_PATH: the path with four-octet AS numbers, from a two-octet session'],
|
||||||
|
'only to customer': ['OTC', 'Only to Customer (RFC 9234): the route came down or across from this AS, and may only go on to customers'],
|
||||||
|
'originator ID': ['OID', 'ORIGINATOR_ID: the router of our AS that learned the route from outside'],
|
||||||
|
'cluster list': ['CL', 'CLUSTER_LIST: the route reflector clusters the route has passed through'],
|
||||||
|
};
|
||||||
const otherList = list => list && list.length
|
const otherList = list => list && list.length
|
||||||
? list.map(a => `<div><span class="dim">${esc(a.name)}</span> <span class="mono">${esc(a.value)}</span></div>`).join('')
|
? list.map(a => {
|
||||||
|
const [text, meaning] = OTHER_GLYPHS[a.name] || [a.name, a.name];
|
||||||
|
return glyph(text, `${meaning}: ${a.value}`);
|
||||||
|
}).join('')
|
||||||
: DASH;
|
: DASH;
|
||||||
|
|
||||||
// An AS number, whose holder's name is shown when it is pointed at.
|
// An AS number, whose holder's name is shown when it is pointed at.
|
||||||
@@ -242,43 +315,136 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
tick();
|
tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATE_CLASS = { Established: 'ok', Connecting: 'warn', Backoff: 'bad', Idle: 'dim', Passive: 'dim' };
|
// Where a peer stands, for the strip and for sorting: 0 failed, 1
|
||||||
// Where several routes for a prefix are exchanged (ADD-PATH), and which way.
|
// degraded, 2 normal, 3 inactive and expected.
|
||||||
function addPath(p) {
|
function peerRank(p) {
|
||||||
const rx = p.add_path_receive || [], tx = p.add_path_send || [];
|
if (p.state === 'Backoff' || p.state === 'Connecting') return p.stale_until_unix ? 1 : 0;
|
||||||
if (!rx.length && !tx.length) return '';
|
if (p.state === 'Established') return p.stale_routes ? 1 : 2;
|
||||||
const parts = [];
|
return 3;
|
||||||
if (rx.length) parts.push(`receive ${esc(rx.join(', '))}`);
|
|
||||||
if (tx.length) parts.push(`send ${esc(tx.join(', '))}`);
|
|
||||||
return ` <span class="dim">· add-path: ${parts.join('; ')}</span>`;
|
|
||||||
}
|
}
|
||||||
|
const ROLE_GLYPH = {
|
||||||
|
provider: ['↑', 'provider: we buy transit from it'],
|
||||||
|
customer: ['↓', 'customer: it buys transit from us'],
|
||||||
|
peer: ['↔', 'peer: we exchange our own and our customers’ routes'],
|
||||||
|
'route server': ['RS', 'route server of an Internet exchange'],
|
||||||
|
'route server client': ['RSC', 'client of a route server we run'],
|
||||||
|
};
|
||||||
|
function roleGlyph(p) {
|
||||||
|
if (p.peer_asn === (state.status || {}).local_asn) return glyph('iBGP', 'a router of our own AS');
|
||||||
|
const r = ROLE_GLYPH[p.role];
|
||||||
|
if (!r) return glyph('?', 'no role configured', 'amber');
|
||||||
|
const confirmed = p.role_confirmed ? '; the peer says so too (RFC 9234)' : p.state === 'Established' ? '; by our configuration, the peer has not said what it is' : '';
|
||||||
|
return glyph(r[0], r[1] + confirmed);
|
||||||
|
}
|
||||||
|
// The session's capabilities as glyphs.
|
||||||
|
function sessionGlyphs(p) {
|
||||||
|
const out = [];
|
||||||
|
const families = p.address_families || [];
|
||||||
|
if (families.some(f => f.startsWith('IPv4'))) out.push(glyph('v4', 'IPv4 unicast'));
|
||||||
|
if (families.some(f => f.startsWith('IPv6'))) out.push(glyph('v6', 'IPv6 unicast'));
|
||||||
|
if (p.extended_next_hop) out.push(glyph('ENH', 'IPv4 routes go with IPv6 next hops (RFC 8950)'));
|
||||||
|
const gr = p.graceful_restart;
|
||||||
|
if (gr) out.push(glyph('GR', `Graceful restart (RFC 4724): the peer says a restart takes ${gr.restart_time_secs}s${gr.notification ? '; NOTIFICATIONs other than Hard Reset count as restarts (RFC 8538)' : ''}`));
|
||||||
|
const rx = p.add_path_receive || [], tx = p.add_path_send || [];
|
||||||
|
if (rx.length || tx.length) out.push(glyph(`AP${rx.length ? '↓' : ''}${tx.length ? '↑' : ''}`, `ADD-PATH (RFC 7911): ${rx.length ? `we receive several paths for ${rx.join(', ')}` : ''}${rx.length && tx.length ? '; ' : ''}${tx.length ? `we send several for ${tx.join(', ')}` : ''}`));
|
||||||
|
if (p.four_octet_asn === false) out.push(glyph('2B', 'The peer handles two-octet AS numbers only (no RFC 6793): wide AS numbers go as AS_TRANS', 'amber'));
|
||||||
|
if (p.direction) out.push(glyph(p.direction === 'outbound' ? '⇢' : '⇠', p.direction === 'outbound' ? 'the connection was opened by us' : 'the connection was opened by the peer', 'dim'));
|
||||||
|
return out.join('');
|
||||||
|
}
|
||||||
|
const STATE_CLASS = { Established: '', Connecting: 'amber', Backoff: 'red', Idle: 'dim', Passive: 'dim' };
|
||||||
|
function peerDetail(p) {
|
||||||
|
const parts = [];
|
||||||
|
if (p.state !== 'Established' && p.stale_until_unix)
|
||||||
|
parts.push(`<span class="amber">restarting</span>: ${fmtCount(p.stale_routes)} routes kept for ${until(p.stale_until_unix)}`);
|
||||||
|
if (p.state === 'Backoff') parts.push(`${esc(p.last_error || '')}; retry in ${until(p.retry_at_unix)}`);
|
||||||
|
if (p.state === 'Passive') parts.push(`waiting for the peer to connect${p.last_error ? `; last session: ${esc(p.last_error)}` : ''}`);
|
||||||
|
return parts.join('; ');
|
||||||
|
}
|
||||||
|
const ROLE_ORDER = ['iBGP', 'provider', 'route server', 'peer', 'customer', 'route server client', 'none'];
|
||||||
|
function peerGroup(p) {
|
||||||
|
if (p.peer_asn === (state.status || {}).local_asn) return 'iBGP';
|
||||||
|
return p.role || 'none';
|
||||||
|
}
|
||||||
|
const GROUP_TITLE = { iBGP: 'Our own routers', provider: 'Providers', 'route server': 'Route servers', peer: 'Peers', customer: 'Customers', 'route server client': 'Route server clients', none: 'Without a role' };
|
||||||
function renderPeers() {
|
function renderPeers() {
|
||||||
const b = $('peers-body');
|
const b = $('peers-body');
|
||||||
$('peers-count').textContent = `${state.peers.filter(p => p.state === 'Established').length} of ${state.peers.length} established`;
|
const peers = state.peers;
|
||||||
if (!state.peers.length) { setHTML(b, '<tr><td colspan="11" class="dim">No peers configured and none connected.</td></tr>'); return; }
|
const up = peers.filter(p => p.state === 'Established').length;
|
||||||
setHTML(b, state.peers.map(p => {
|
const down = peers.filter(p => peerRank(p) === 0).length;
|
||||||
|
const waiting = peers.filter(p => p.state === 'Passive').length;
|
||||||
|
$('peers-count').textContent = `${up} of ${peers.length} established` + (down ? `, ${down} down` : '') + (waiting ? `, ${waiting} waiting` : '');
|
||||||
|
if (!peers.length) { setHTML(b, '<tr><td colspan="9" class="dim">No peers configured and none connected.</td></tr>'); return; }
|
||||||
|
const problemsOnly = $('peers-problems').checked;
|
||||||
|
const row = p => {
|
||||||
const up = p.state === 'Established';
|
const up = p.state === 'Established';
|
||||||
const restarting = !up && p.stale_until_unix
|
const name = p.name ? `<a href="#received" class="peer-link" data-peer="${esc(p.name)}" title="Look at this peer's routes">${esc(p.name)}</a>` : '<span class="dim">(inbound)</span>';
|
||||||
? `<span class="warn" title="Graceful restart: the peer's routes are kept, and used, until it is back and has sent them again">restarting: ${p.stale_routes} routes kept for ${until(p.stale_until_unix)}</span>` : '';
|
const rejected = up ? `<span class="dim">${fmtCount(p.rejected_routes || 0)}</span>` : DASH;
|
||||||
const detail = [restarting,
|
const held = up && p.held_routes ? ` · <span class="amber" title="Routes rejected as RPKI or ASPA invalid, or for a next hop that cannot be reached, and kept aside, to be taken if that changes">${fmtCount(p.held_routes)} held</span>` : '';
|
||||||
p.state === 'Backoff' ? `${esc(p.last_error || '')}; retry in ${until(p.retry_at_unix)}` : '',
|
const stale = up && p.stale_routes ? ` ${glyph(`${fmtCount(p.stale_routes)} stale`, 'Kept from the peer’s last session, and not sent again yet', 'amber')}` : '';
|
||||||
p.state === 'Passive' ? `waiting for the peer to connect${p.last_error ? `; last session: ${esc(p.last_error)}` : ''}` : '']
|
return `<tr class="${peerRank(p) === 3 ? 'dim' : ''}">
|
||||||
.filter(Boolean).join('; ');
|
<td class="name">${name}</td>
|
||||||
const gr = p.graceful_restart;
|
<td class="mono">${esc(p.peer_addr)} <span class="dim">AS${asn(p.peer_asn)}</span> ${roleGlyph(p)}</td>
|
||||||
const graceful = gr ? ` · <span title="Graceful restart (RFC 4724): the peer says a restart takes ${gr.restart_time_secs}s${gr.notification ? ', and NOTIFICATIONs other than Hard Reset count as restarts (RFC 8538)' : ''}">graceful restart</span>` : '';
|
<td><span class="${STATE_CLASS[p.state] || ''}">${esc(p.state)}</span></td>
|
||||||
const stale = p.stale_routes ? ` <span class="warn" title="Kept from the peer's last session, and not sent again yet">(${p.stale_routes} stale)</span>` : '';
|
<td class="nowrap">${up ? `<span title="since ${esc(fmtWhen(p.established_at_unix))}">${age(p.established_at_unix)}</span>` : DASH}</td>
|
||||||
return `<tr>
|
<td class="mono">${up ? esc(p.local_addr) : DASH}</td>
|
||||||
<td>${p.name ? esc(p.name) : '<span class="dim">(inbound)</span>'}</td><td class="mono">${esc(p.peer_addr)}</td><td>${asn(p.peer_asn)}${p.role ? ` <span class="dim" title="${p.role_confirmed ? 'The peer says so too (RFC 9234)' : 'By our configuration; the peer has not said what it is'}">${esc(p.role)}${p.role_confirmed ? ' ✓' : ''}</span>` : ''}</td>
|
<td>${up ? sessionGlyphs(p) : DASH}</td>
|
||||||
<td><span class="${STATE_CLASS[p.state] || ''}">${esc(p.state)}</span></td><td>${up ? (p.direction === 'outbound' ? 'us' : 'peer') : DASH}</td>
|
<td class="num">${up ? fmtCount(p.routes) + stale : DASH}</td>
|
||||||
<td class="mono">${up ? esc(p.local_addr) : DASH}</td><td>${up ? fmtTime(p.established_at_unix) : DASH}</td>
|
<td class="num">${rejected}${held}</td>
|
||||||
<td>${up ? age(p.established_at_unix) : DASH}</td><td>${up ? (p.four_octet_asn ? 'yes' : 'no') : DASH}</td>
|
<td>${peerDetail(p)}</td></tr>`;
|
||||||
<td>${up ? esc((p.address_families || []).join(', ')) + addPath(p) + (p.extended_next_hop ? ' · IPv4 with IPv6 next hops' : '') + graceful : DASH}</td>
|
};
|
||||||
<td>${up ? p.routes + stale : DASH}</td>
|
const byName = (a, c) => (a.name || a.peer_addr).localeCompare(c.name || c.peer_addr);
|
||||||
<td>${up ? (p.rejected_routes ? `<a class="bad" href="#rejected">${p.rejected_routes}</a>` : 0) + (p.held_routes ? ` · <span title="Routes rejected as RPKI or ASPA invalid, or for a next hop that cannot be reached, and kept aside, to be taken if that changes">${p.held_routes} held now</span>` : '') : DASH}</td><td>${detail}</td></tr>`;
|
let html = '';
|
||||||
}).join(''));
|
// What needs attention first, whoever it is; then everyone by what
|
||||||
|
// they are to us.
|
||||||
|
const problems = peers.filter(p => peerRank(p) <= 1).sort(byName);
|
||||||
|
if (problems.length) {
|
||||||
|
html += `<tr class="group"><td colspan="9"><span class="red">Needing attention</span> (${problems.length})</td></tr>` + problems.map(row).join('');
|
||||||
|
}
|
||||||
|
if (!problemsOnly) {
|
||||||
|
for (const group of ROLE_ORDER) {
|
||||||
|
const members = peers.filter(p => peerRank(p) >= 2 && peerGroup(p) === group).sort(byName);
|
||||||
|
if (!members.length) continue;
|
||||||
|
html += `<tr class="group"><td colspan="9">${GROUP_TITLE[group]} (${members.length})</td></tr>` + members.map(row).join('');
|
||||||
|
}
|
||||||
|
} else if (!problems.length) {
|
||||||
|
html += '<tr><td colspan="9" class="dim">Nothing needs attention.</td></tr>';
|
||||||
|
}
|
||||||
|
setHTML(b, html);
|
||||||
renderPeerChoices();
|
renderPeerChoices();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The strip: what needs attention, in one line, or that nothing does.
|
||||||
|
function renderStrip() {
|
||||||
|
const items = [];
|
||||||
|
const peers = state.peers, k = state.kernel, r = state.rpki;
|
||||||
|
const down = peers.filter(p => peerRank(p) === 0);
|
||||||
|
if (down.length) items.push(`<span class="red">${down.length} session${down.length === 1 ? '' : 's'} down:</span> ${down.map(p => `<b>${esc(p.name || p.peer_addr)}</b> <span class="dim">(${esc((p.last_error || p.state).replace(/;.*$/, ''))})</span>`).join(', ')}`);
|
||||||
|
const restarting = peers.filter(p => peerRank(p) === 1);
|
||||||
|
if (restarting.length) items.push(`<span class="amber">${restarting.length} restarting or stale:</span> ${restarting.map(p => `<b>${esc(p.name || p.peer_addr)}</b>`).join(', ')}`);
|
||||||
|
if (k && k.enabled) {
|
||||||
|
if (!k.synced) items.push('<span class="amber">kernel catching up</span> with the routing table');
|
||||||
|
if (k.refused) items.push(`<span class="red">kernel refused ${fmtCount(k.refused)} routes</span>`);
|
||||||
|
const unresolved = Object.values(k.unresolved || {}).reduce((a, n) => a + n, 0);
|
||||||
|
if (unresolved) items.push(`<span class="amber">${fmtCount(unresolved)} best routes not installed</span> (next hop unusable)`);
|
||||||
|
}
|
||||||
|
if (r && r.enabled) {
|
||||||
|
if (r.state !== 'synchronised') items.push(`<span class="red">RPKI ${esc(r.state)}</span>${r.last_error ? ` <span class="dim">(${esc(r.last_error)})</span>` : ''}`);
|
||||||
|
else if (!r.updated_at_unix) items.push('<span class="amber">RPKI has no data</span>: nothing is refused as invalid');
|
||||||
|
else if (r.expires_at_unix && r.expires_at_unix - now() < 3600) items.push(`<span class="amber">RPKI data expires in ${until(r.expires_at_unix)}</span> unless confirmed`);
|
||||||
|
}
|
||||||
|
const held = peers.reduce((a, p) => a + (p.held_routes || 0), 0);
|
||||||
|
const normal = [];
|
||||||
|
const up = peers.filter(p => p.state === 'Established').length;
|
||||||
|
normal.push(`${up}/${peers.length} sessions up`);
|
||||||
|
if (k && k.enabled) normal.push(`${fmtCount(k.installed)} routes in the kernel${k.synced ? ', <span class="green">in step</span>' : ''}`);
|
||||||
|
if (r && r.enabled && r.state === 'synchronised' && r.updated_at_unix) normal.push(`RPKI <span class="green">current</span>`);
|
||||||
|
if (held) normal.push(`<span class="dim">${fmtCount(held)} routes held</span>`);
|
||||||
|
const waiting = peers.filter(p => p.state === 'Passive').length;
|
||||||
|
if (waiting) normal.push(`<span class="dim">${waiting} waiting to connect</span>`);
|
||||||
|
const head = items.length ? items.map(i => `<span class="item">${i}</span>`).join('') : '<span class="item"><span class="dim">Nothing needs attention.</span></span>';
|
||||||
|
setHTML($('strip'), head + `<br><span class="dim">${normal.join(' · ')}</span>`);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- looking routes up -------------------------------------------------
|
// ---- looking routes up -------------------------------------------------
|
||||||
// The query lives in the page's address, so that it survives a reload and
|
// The query lives in the page's address, so that it survives a reload and
|
||||||
// auto-refresh, and can be passed on.
|
// auto-refresh, and can be passed on.
|
||||||
@@ -321,7 +487,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
function renderPeerChoices() {
|
function renderPeerChoices() {
|
||||||
const chosen = lookup().peer;
|
const chosen = lookup().peer;
|
||||||
const names = [...new Set(state.peers.filter(p => p.state === 'Established')
|
const names = [...new Set(state.peers.filter(p => p.state === 'Established')
|
||||||
.map(p => p.name || p.peer_addr.replace(/:\d+$/, '').replace(/^\[|\]$/g, '')))];
|
.map(p => p.name || p.peer_addr.replace(/:\d+$/, '').replace(/^\[|\]$/g, '')))].sort();
|
||||||
if (chosen && !names.includes(chosen)) names.push(chosen);
|
if (chosen && !names.includes(chosen)) names.push(chosen);
|
||||||
setHTML($('lookup-peer'), '<option value="">any peer</option>' +
|
setHTML($('lookup-peer'), '<option value="">any peer</option>' +
|
||||||
names.map(n => `<option value="${esc(n)}"${n === chosen ? ' selected' : ''}>${esc(n)}</option>`).join(''));
|
names.map(n => `<option value="${esc(n)}"${n === chosen ? ' selected' : ''}>${esc(n)}</option>`).join(''));
|
||||||
@@ -344,6 +510,14 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
history.replaceState(null, '', url.pathname + url.search + '#received');
|
history.replaceState(null, '', url.pathname + url.search + '#received');
|
||||||
showLookup(); renderRefreshControl();
|
showLookup(); renderRefreshControl();
|
||||||
loadReceived().catch(console.error);
|
loadReceived().catch(console.error);
|
||||||
|
renderRejected();
|
||||||
|
}
|
||||||
|
// What was typed is a peer's name if a peer has it: then it is the peer
|
||||||
|
// to look at, not a prefix.
|
||||||
|
function submitLookup() {
|
||||||
|
const typed = $('lookup-q').value.trim();
|
||||||
|
const isPeer = state.peers.some(p => p.name === typed);
|
||||||
|
setLookup({ q: isPeer ? '' : typed, mode: $('lookup-mode').value, peer: isPeer ? typed : $('lookup-peer').value, best: $('lookup-best').checked });
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLocal() {
|
function renderLocal() {
|
||||||
@@ -352,46 +526,56 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
const local = state.status ? state.status.local_asn : '?';
|
const local = state.status ? state.status.local_asn : '?';
|
||||||
setHTML(b, state.local.map(r => `<tr>
|
setHTML(b, state.local.map(r => `<tr>
|
||||||
<td class="mono">${esc(r.prefix)}</td><td>${esc(r.origin)}</td>
|
<td class="mono">${esc(r.prefix)}</td><td>${esc(r.origin)}</td>
|
||||||
<td class="mono path">${asPath(Array(1 + r.prepend).fill(local))}</td><td>${r.med ?? DASH}</td>
|
<td class="mono path">${asPath(Array(1 + r.prepend).fill(local))}</td><td class="num">${r.med ?? DASH}</td>
|
||||||
<td>${commList(r.communities)}</td><td>${commList(r.large_communities, true)}</td>
|
<td>${commList(r.communities)}</td><td>${commList(r.large_communities, true)}</td>
|
||||||
<td>${age(r.announced_at_unix)}</td></tr>`).join(''));
|
<td>${age(r.announced_at_unix)}</td></tr>`).join(''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VALIDITY = {
|
||||||
|
valid: ['✓', 'green'], invalid: ['✗', 'red'], 'not found': ['–', 'dim'], unknown: ['–', 'dim'],
|
||||||
|
};
|
||||||
|
function validity(what, value) {
|
||||||
|
if (!value) return `<span class="dim" title="${esc(what)}: not checked">·</span>`;
|
||||||
|
const [mark, cls] = VALIDITY[value] || [value, ''];
|
||||||
|
return `<span class="${cls}" title="${esc(what)}: ${esc(value)}">${mark}</span>`;
|
||||||
|
}
|
||||||
function renderReceived() {
|
function renderReceived() {
|
||||||
const b = $('received-body'), st = state.status, looking = isLookup();
|
const b = $('received-body'), st = state.status, looking = isLookup();
|
||||||
const page = state.receivedPage, paged = !!(page.next_after || page.prev_before);
|
const page = state.receivedPage, paged = !!(page.next_after || page.prev_before);
|
||||||
const shown = paged ? `${state.received.length} on this page` : `${state.received.length}`;
|
const shown = paged ? `${fmtCount(state.received.length)} on this page` : fmtCount(state.received.length);
|
||||||
$('received-count').textContent = looking
|
$('received-count').textContent = looking
|
||||||
? `${shown}${paged ? '' : ' found'}${st ? ` among ${st.received_routes}` : ''}`
|
? `${shown}${paged ? '' : ' found'}${st ? ` among ${fmtCount(st.received_routes)}` : ''}`
|
||||||
: paged && st
|
: paged && st
|
||||||
? `${st.received_routes}, for ${st.received_prefixes} prefixes; ${shown}`
|
? `${fmtCount(st.received_routes)}, for ${fmtCount(st.received_prefixes)} prefixes; ${shown}`
|
||||||
: `${shown}${st ? `, for ${st.received_prefixes} prefixes` : ''}`;
|
: `${shown}${st ? `, for ${fmtCount(st.received_prefixes)} prefixes` : ''}`;
|
||||||
renderPager();
|
renderPager();
|
||||||
if (!state.received.length) {
|
if (!state.received.length) {
|
||||||
setHTML(b, `<tr><td colspan="13" class="dim">${looking ? 'No route matches.' : 'No routes received from peers.'}</td></tr>`); return;
|
setHTML(b, `<tr><td colspan="12" class="dim">${looking ? 'No route matches.' : 'No routes received from peers.'}</td></tr>`); return;
|
||||||
}
|
}
|
||||||
setHTML(b, state.received.map(r => { const s = r.summary; return `<tr>
|
setHTML(b, state.received.map(r => { const s = r.summary; return `<tr class="${r.best ? 'best' : ''}">
|
||||||
<td class="mono">${esc(r.prefix)}</td><td class="mono">${r.peer_name ? esc(r.peer_name) + ' ' : ''}${esc(r.peer_addr)} <span class="dim">AS${asn(r.peer_asn)}${r.path_id ? ` path ${r.path_id}` : ''}</span></td><td class="mono path">${asPath(s.as_path)}</td>
|
<td>${r.best ? glyph('★', 'The best route for this prefix: the one chosen, installed and passed on', 'green') : ''}${r.stale ? glyph('stale', 'Kept from a session of the peer’s that ended (graceful restart), and not sent again since', 'amber') : ''}</td>
|
||||||
<td class="mono">${s.next_hop ? esc(s.next_hop) : DASH}</td><td>${s.origin ? esc(s.origin) : DASH}</td><td>${s.med ?? DASH}</td><td>${s.local_pref ?? DASH}</td>
|
<td class="mono">${esc(r.prefix)}</td>
|
||||||
|
<td class="mono">${r.peer_name ? `<a href="#received" class="peer-link" data-peer="${esc(r.peer_name)}">${esc(r.peer_name)}</a> ` : ''}<span class="dim">${esc(r.peer_addr)} AS${asn(r.peer_asn)}${r.path_id ? ` path ${r.path_id}` : ''}</span></td>
|
||||||
|
<td class="mono path">${asPath(s.as_path)}</td>
|
||||||
|
<td class="mono">${s.next_hop ? esc(s.next_hop) : DASH}</td><td>${s.origin ? esc(s.origin) : DASH}</td>
|
||||||
|
<td class="num">${s.med ?? DASH}</td><td class="num">${s.local_pref ?? DASH}</td>
|
||||||
<td>${commList(s.communities)}</td><td>${commList(s.large_communities, true)}</td><td>${otherList(s.other)}</td>
|
<td>${commList(s.communities)}</td><td>${commList(s.large_communities, true)}</td><td>${otherList(s.other)}</td>
|
||||||
<td>${r.rpki ? `<span class="${RPKI_CLASS[r.rpki] || ''}">${esc(r.rpki)}</span>` : DASH}</td>
|
<td class="nowrap">${validity('RPKI', r.rpki)} ${validity('ASPA', r.aspa)}</td></tr>`; }).join(''));
|
||||||
<td>${r.aspa ? `<span class="${RPKI_CLASS[r.aspa] || ''}">${esc(r.aspa)}</span>` : DASH}</td><td>${r.best ? '<span class="ok">best</span>' : ''}${r.stale ? ` <span class="warn" title="Kept from a session of the peer's that ended (graceful restart), and not sent again since">stale</span>` : ''}</td></tr>`; }).join(''));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const RPKI_CLASS = { valid: 'ok', invalid: 'bad', 'not found': 'dim', unknown: 'dim' };
|
const RPKI_STATE_CLASS = { synchronised: 'green', synchronising: 'amber', connecting: 'red' };
|
||||||
const RPKI_STATE_CLASS = { synchronised: 'ok', synchronising: 'warn', connecting: 'bad' };
|
|
||||||
function renderRpki() {
|
function renderRpki() {
|
||||||
const r = state.rpki, b = $('rpki-body');
|
const r = state.rpki, b = $('rpki-body');
|
||||||
if (!r) return;
|
if (!r) return;
|
||||||
if (!r.enabled) { setHTML(b, '<span class="dim">Not configured: routes are not validated.</span>'); return; }
|
if (!r.enabled) { setHTML(b, '<span class="dim">Not configured: routes are not validated.</span>'); return; }
|
||||||
const data = r.updated_at_unix
|
const data = r.updated_at_unix
|
||||||
? `${r.vrps_ipv4} IPv4 and ${r.vrps_ipv6} IPv6 VRPs and ${r.aspas} ASPAs at serial ${r.serial ?? '?'}, confirmed ${age(r.updated_at_unix)} ago` +
|
? `${fmtCount(r.vrps_ipv4)} IPv4 and ${fmtCount(r.vrps_ipv6)} IPv6 VRPs and ${fmtCount(r.aspas)} ASPAs at serial ${r.serial ?? '?'}, confirmed ${age(r.updated_at_unix)} ago` +
|
||||||
(r.expires_at_unix ? `, dropped in ${until(r.expires_at_unix)} unless confirmed again` : '')
|
(r.expires_at_unix ? `, dropped in ${until(r.expires_at_unix)} unless confirmed again` : '')
|
||||||
: '<span class="warn">no data: every route is "not found" and none is rejected as invalid</span>';
|
: '<span class="amber">no data: every route is "not found" and none is refused as invalid</span>';
|
||||||
setHTML(b, `Cache <span class="mono">${esc(r.cache || '?')}</span>: <span class="${RPKI_STATE_CLASS[r.state] || ''}">${esc(r.state)}</span>` +
|
setHTML(b, `Cache <span class="mono">${esc(r.cache || '?')}</span>: <span class="${RPKI_STATE_CLASS[r.state] || ''}">${esc(r.state)}</span>` +
|
||||||
` <span class="dim">(RTR version ${r.protocol_version})</span>. ${data}.` +
|
` <span class="dim">(RTR version ${r.protocol_version})</span>. ${data}.` +
|
||||||
(r.updated_at_unix && r.protocol_version < 2 ? '<br><span class="warn">This cache speaks an RTR version without ASPAs: AS paths are not verified.</span>' : '') +
|
(r.updated_at_unix && r.protocol_version < 2 ? '<br><span class="amber">This cache speaks an RTR version without ASPAs: AS paths are not verified.</span>' : '') +
|
||||||
(r.last_error ? `<br><span class="bad">Last error:</span> ${esc(r.last_error)}` : ''));
|
(r.last_error ? `<br><span class="red">Last error:</span> ${esc(r.last_error)}` : ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderKernel() {
|
function renderKernel() {
|
||||||
@@ -401,13 +585,13 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
const row = (name, value) => `<tr><th>${name}</th><td>${value}</td></tr>`;
|
const row = (name, value) => `<tr><th>${name}</th><td>${value}</td></tr>`;
|
||||||
const unresolved = Object.entries(k.unresolved).map(([why, n]) => `${fmtCount(n)} ${esc(why)}`).join(', ');
|
const unresolved = Object.entries(k.unresolved).map(([why, n]) => `${fmtCount(n)} ${esc(why)}`).join(', ');
|
||||||
const summary = '<table class="data kv">' +
|
const summary = '<table class="data kv">' +
|
||||||
row('state', `<span class="${k.synced ? 'ok' : 'warn'}">${k.synced ? 'in step' : 'catching up'}</span> with the routing table`) +
|
row('state', `<span class="${k.synced ? 'green' : 'amber'}">${k.synced ? 'in step' : 'catching up'}</span> with the routing table`) +
|
||||||
row('routes installed', `<b>${fmtCount(k.installed)}</b>`) +
|
row('routes installed', `<b>${fmtCount(k.installed)}</b>`) +
|
||||||
row('installed as', `<span class="mono">table ${k.table} metric ${k.metric} proto bgp</span>`) +
|
row('installed as', `<span class="mono">table ${k.table} metric ${k.metric} proto bgp</span>`) +
|
||||||
(k.kept ? row('kept from before the restart', `<span class="warn">${fmtCount(k.kept)}</span>, not announced again yet`) : '') +
|
(k.kept ? row('kept from before the restart', `<span class="amber">${fmtCount(k.kept)}</span>, not announced again yet`) : '') +
|
||||||
(unresolved ? row('not installed', `<span class="warn">${unresolved}</span>`) : '') +
|
(unresolved ? row('not installed', `<span class="amber">${unresolved}</span>`) : '') +
|
||||||
(k.rejected ? row('rejected by the kernel', `<span class="bad">${fmtCount(k.rejected)}</span>`) : '') +
|
(k.refused ? row('refused by the kernel', `<span class="red">${fmtCount(k.refused)}</span>`) : '') +
|
||||||
(k.last_error ? row('last error', `<span class="bad">${esc(k.last_error)}</span> (${fmtCount(k.errors)} errors in all)`) : '') +
|
(k.last_error ? row('last error', `<span class="red">${esc(k.last_error)}</span> (${fmtCount(k.errors)} errors in all)`) : '') +
|
||||||
'</table>';
|
'</table>';
|
||||||
// Interfaces that are up or have addresses get a row each; the others
|
// Interfaces that are up or have addresses get a row each; the others
|
||||||
// (unused ports, mostly) one line between them.
|
// (unused ports, mostly) one line between them.
|
||||||
@@ -418,7 +602,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
const interfaces = !k.interfaces.length ? '' :
|
const interfaces = !k.interfaces.length ? '' :
|
||||||
'<table class="data"><thead><tr><th>interface</th><th>state</th><th>IPv4</th><th>IPv6</th><th>IPv6 link-local</th></tr></thead><tbody>' +
|
'<table class="data"><thead><tr><th>interface</th><th>state</th><th>IPv4</th><th>IPv6</th><th>IPv6 link-local</th></tr></thead><tbody>' +
|
||||||
listed.map(i => `<tr${i.up ? '' : ' class="dim"'}><td class="mono">${esc(i.name)}</td>
|
listed.map(i => `<tr${i.up ? '' : ' class="dim"'}><td class="mono">${esc(i.name)}</td>
|
||||||
<td><span class="${i.up ? 'ok' : 'bad'}">${i.up ? 'up' : 'down'}</span></td>
|
<td>${i.up ? glyph('▲ up', 'the interface is up', 'green') : glyph('▼ down', 'the interface is down', 'red')}</td>
|
||||||
<td class="mono">${lines(i.addresses.filter(v4))}</td>
|
<td class="mono">${lines(i.addresses.filter(v4))}</td>
|
||||||
<td class="mono">${lines(i.addresses.filter(a => !v4(a) && !linkLocal(a)))}</td>
|
<td class="mono">${lines(i.addresses.filter(a => !v4(a) && !linkLocal(a)))}</td>
|
||||||
<td class="mono dim">${lines(i.addresses.filter(linkLocal))}</td></tr>`).join('') +
|
<td class="mono dim">${lines(i.addresses.filter(linkLocal))}</td></tr>`).join('') +
|
||||||
@@ -427,31 +611,54 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
setHTML(b, summary + interfaces);
|
setHTML(b, summary + interfaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each peer a line that says how many and why, then its latest few.
|
// The breakdown of a peer's rejections by reason: a bar, each reason a
|
||||||
|
// segment, with the counts after it.
|
||||||
|
const REASON_COLOURS = ['#88a', '#a60', '#6a6', '#c66', '#69c', '#996', '#a6a', '#999'];
|
||||||
|
function reasonBar(byReason, total) {
|
||||||
|
const reasons = Object.entries(byReason).sort((a, c) => c[1] - a[1]);
|
||||||
|
const bar = reasons.map(([reason, n], i) =>
|
||||||
|
`<span style="width: ${100 * n / total}%; background: ${REASON_COLOURS[i % REASON_COLOURS.length]}" title="${esc(reason)}: ${fmtCount(n)}"></span>`).join('');
|
||||||
|
const legend = reasons.map(([reason, n], i) =>
|
||||||
|
`<span class="nowrap"><span style="display: inline-block; width: 8px; height: 8px; background: ${REASON_COLOURS[i % REASON_COLOURS.length]}"></span> ${fmtCount(n)} ${esc(reason)}</span>`).join(', ');
|
||||||
|
return `<span class="bar">${bar}</span>${legend}`;
|
||||||
|
}
|
||||||
|
// For each peer a line that says how many and why, then its latest
|
||||||
|
// rejections with those alike as one line.
|
||||||
const REJECTED_SHOWN = 20;
|
const REJECTED_SHOWN = 20;
|
||||||
function renderRefused() {
|
function renderRejected() {
|
||||||
const b = $('rejected-body');
|
const b = $('rejected-body');
|
||||||
|
const chosen = lookup().peer;
|
||||||
|
const peers = chosen ? state.rejected.filter(p => p.peer === chosen) : state.rejected;
|
||||||
const total = state.rejected.reduce((n, p) => n + p.total, 0);
|
const total = state.rejected.reduce((n, p) => n + p.total, 0);
|
||||||
const announcements = n => `${n} announcement${n === 1 ? '' : 's'}`;
|
const announcements = n => `${fmtCount(n)} announcement${n === 1 ? '' : 's'}`;
|
||||||
$('rejected-count').textContent = total ? `${announcements(total)} from ${state.rejected.length} peer${state.rejected.length === 1 ? '' : 's'}` : '0';
|
$('rejected-count').textContent = total ? `${announcements(total)} from ${state.rejected.length} peer${state.rejected.length === 1 ? '' : 's'}${chosen ? `; showing ${chosen}` : ''}` : '0';
|
||||||
if (!state.rejected.length) { setHTML(b, '<tr><td colspan="12" class="dim">Nothing a peer announced has been rejected.</td></tr>'); return; }
|
if (!peers.length) { setHTML(b, `<tr><td colspan="11" class="dim">${chosen ? `Nothing ${esc(chosen)} announced has been rejected.` : 'Nothing a peer announced has been rejected.'}</td></tr>`); return; }
|
||||||
setHTML(b, state.rejected.map(p => {
|
setHTML(b, peers.map(p => {
|
||||||
const why = Object.entries(p.by_reason).sort((a, c) => c[1] - a[1]).map(([reason, n]) => `${n} ${esc(reason)}`).join(', ');
|
// Alike: the same prefix, for the same reason, by the same path.
|
||||||
const more = p.recent.length > REJECTED_SHOWN
|
const alike = new Map();
|
||||||
? ` <span class="dim">Latest ${REJECTED_SHOWN} shown; <a href="/api/routes/rejected?peer=${encodeURIComponent(p.peer)}">the latest ${p.recent.length} as json</a>.</span>` : '';
|
for (const r of p.recent) {
|
||||||
return `<tr><td colspan="12"><b>${esc(p.peer)}</b> <span class="dim">${esc(p.peer_addr)}</span>: <span class="bad">${announcements(p.total)} rejected</span> (${why}).${more}</td></tr>` +
|
const key = `${r.prefix}|${r.reason}|${r.as_path.join(' ')}`;
|
||||||
p.recent.slice(0, REJECTED_SHOWN).map(r => { const s = r.summary; return `<tr><td></td>
|
const seen = alike.get(key);
|
||||||
|
if (seen) { seen.times += 1; seen.first = Math.min(seen.first, r.at_unix); }
|
||||||
|
else alike.set(key, { ...r, times: 1, first: r.at_unix });
|
||||||
|
}
|
||||||
|
const lines = [...alike.values()].sort((a, c) => c.at_unix - a.at_unix).slice(0, REJECTED_SHOWN);
|
||||||
|
const more = alike.size > REJECTED_SHOWN
|
||||||
|
? ` <span class="dim">${REJECTED_SHOWN} of ${alike.size} shown; <a href="/api/routes/rejected?peer=${encodeURIComponent(p.peer)}">the latest ${p.recent.length} as json</a>.</span>` : '';
|
||||||
|
return `<tr class="group"><td colspan="11"><a href="#received" class="peer-link" data-peer="${esc(p.peer)}">${esc(p.peer)}</a> <span class="dim" style="font-weight: normal">${esc(p.peer_addr)}</span>: <span style="font-weight: normal">${announcements(p.total)} rejected</span> ${reasonBar(p.by_reason, p.total)}${more}</td></tr>` +
|
||||||
|
lines.map(r => { const s = r.summary; return `<tr><td></td>
|
||||||
<td class="mono">${esc(r.prefix)}${r.path_id ? ` <span class="dim">path ${r.path_id}</span>` : ''}</td>
|
<td class="mono">${esc(r.prefix)}${r.path_id ? ` <span class="dim">path ${r.path_id}</span>` : ''}</td>
|
||||||
<td class="mono path">${asPath(r.as_path)}</td><td>${esc(r.reason)}</td><td class="nowrap">${age(r.at_unix)} ago</td>
|
<td class="mono path">${asPath(r.as_path)}</td><td>${esc(r.reason)}</td>
|
||||||
|
<td class="num">${r.times > 1 ? `×${r.times}` : ''}</td>
|
||||||
|
<td class="nowrap"><span title="${esc(fmtWhen(r.at_unix))}${r.times > 1 ? `; the first of these ${esc(fmtWhen(r.first))}` : ''}">${age(r.at_unix)} ago</span></td>
|
||||||
<td class="mono">${s.next_hop ? esc(s.next_hop) : DASH}</td><td>${s.origin ? esc(s.origin) : DASH}</td>
|
<td class="mono">${s.next_hop ? esc(s.next_hop) : DASH}</td><td>${s.origin ? esc(s.origin) : DASH}</td>
|
||||||
<td>${s.med ?? DASH}</td><td>${s.local_pref ?? DASH}</td>
|
|
||||||
<td>${commList(s.communities)}</td><td>${commList(s.large_communities, true)}</td><td>${otherList(s.other)}</td></tr>`; }).join('');
|
<td>${commList(s.communities)}</td><td>${commList(s.large_communities, true)}</td><td>${otherList(s.other)}</td></tr>`; }).join('');
|
||||||
}).join(''));
|
}).join(''));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- data loading ------------------------------------------------------
|
// ---- data loading ------------------------------------------------------
|
||||||
async function loadStatus() { state.status = await getJSON('/api/status'); renderStatus(); renderLocal(); renderReceived(); }
|
async function loadStatus() { state.status = await getJSON('/api/status'); renderStatus(); renderLocal(); renderReceived(); renderPeers(); renderStrip(); }
|
||||||
async function loadPeers() { state.peers = await getJSON('/api/peers'); renderPeers(); }
|
async function loadPeers() { state.peers = await getJSON('/api/peers'); renderPeers(); renderStrip(); }
|
||||||
// The way to the pages before and after this one, where there are any.
|
// The way to the pages before and after this one, where there are any.
|
||||||
function renderPager() {
|
function renderPager() {
|
||||||
const page = state.receivedPage, place = pagePlace();
|
const page = state.receivedPage, place = pagePlace();
|
||||||
@@ -476,14 +683,14 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// A query the server could not make sense of: say what it said.
|
// A query the server could not make sense of: say what it said.
|
||||||
state.received = []; state.receivedPage = {};
|
state.received = []; state.receivedPage = {};
|
||||||
note.innerHTML = `<span class="bad">${esc(e.message.replace(/^[^:]*: \d+ /, ''))}</span>`;
|
note.innerHTML = `<span class="red">${esc(e.message.replace(/^[^:]*: \d+ /, ''))}</span>`;
|
||||||
}
|
}
|
||||||
renderReceived();
|
renderReceived();
|
||||||
}
|
}
|
||||||
async function loadRefused() { state.rejected = await getJSON('/api/routes/rejected'); renderRefused(); }
|
async function loadRejected() { state.rejected = await getJSON('/api/routes/rejected'); renderRejected(); }
|
||||||
async function loadRpki() { state.rpki = await getJSON('/api/rpki'); renderRpki(); }
|
async function loadRpki() { state.rpki = await getJSON('/api/rpki'); renderRpki(); renderStrip(); }
|
||||||
async function loadKernel() { state.kernel = await getJSON('/api/kernel'); renderKernel(); }
|
async function loadKernel() { state.kernel = await getJSON('/api/kernel'); renderKernel(); renderStrip(); }
|
||||||
async function loadAll() { await Promise.all([loadStatus(), loadPeers(), loadLocal(), loadReceived(), loadRefused(), loadRpki(), loadKernel()]); }
|
async function loadAll() { await Promise.all([loadStatus(), loadPeers(), loadLocal(), loadReceived(), loadRejected(), loadRpki(), loadKernel()]); }
|
||||||
|
|
||||||
// ---- events ------------------------------------------------------------
|
// ---- events ------------------------------------------------------------
|
||||||
function logEvent(html) {
|
function logEvent(html) {
|
||||||
@@ -494,16 +701,16 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
const names = list => list.map(c => WELL_KNOWN[c] || c).map(esc).join(' ');
|
const names = list => list.map(c => WELL_KNOWN[c] || c).map(esc).join(' ');
|
||||||
function describe(ev) {
|
function describe(ev) {
|
||||||
switch (ev.type) {
|
switch (ev.type) {
|
||||||
case 'session_up': return `<span class="ok">session UP</span> with ${esc(ev.peer.name || ev.peer.peer_addr)} AS${ev.peer.peer_asn} (opened by ${ev.peer.direction === 'outbound' ? 'us' : 'peer'})`;
|
case 'session_up': return `<span class="green">session UP</span> with ${esc(ev.peer.name || ev.peer.peer_addr)} AS${ev.peer.peer_asn} (opened by ${ev.peer.direction === 'outbound' ? 'us' : 'peer'})`;
|
||||||
case 'route_refresh_requested': return `${esc(ev.peer.name || ev.peer.peer_addr)} AS${ev.peer.peer_asn} asked for a route refresh; re-sent its routes`;
|
case 'route_refresh_requested': return `${esc(ev.peer.name || ev.peer.peer_addr)} AS${ev.peer.peer_asn} asked for a route refresh; re-sent its routes`;
|
||||||
case 'session_down': return `<span class="bad">session DOWN</span> with ${esc(ev.peer_addr)} AS${ev.peer_asn}`;
|
case 'session_down': return `<span class="red">session DOWN</span> with ${esc(ev.peer_addr)} AS${ev.peer_asn}`;
|
||||||
case 'local_announced': { const r = ev.route; return `originating ${esc(r.prefix)}` +
|
case 'local_announced': { const r = ev.route; return `originating ${esc(r.prefix)}` +
|
||||||
(r.communities.length ? ` [${names(r.communities)}]` : '') + (r.large_communities.length ? ` [${names(r.large_communities)}]` : '') +
|
(r.communities.length ? ` [${names(r.communities)}]` : '') + (r.large_communities.length ? ` [${names(r.large_communities)}]` : '') +
|
||||||
(r.prepend ? ` prepend ${r.prepend}` : '') + (r.med != null ? ` med ${r.med}` : ''); }
|
(r.prepend ? ` prepend ${r.prepend}` : '') + (r.med != null ? ` med ${r.med}` : ''); }
|
||||||
case 'local_withdrawn': return `<span class="warn">no longer originating</span> ${esc(ev.prefix)}`;
|
case 'local_withdrawn': return `<span class="amber">no longer originating</span> ${esc(ev.prefix)}`;
|
||||||
case 'best_changed': return `best route for ${esc(ev.route.prefix)} is now from AS${ev.route.peer_asn} path [${ev.route.summary.as_path.join(' ')}]`;
|
case 'best_changed': return `best route for ${esc(ev.route.prefix)} is now from AS${ev.route.peer_asn} path [${ev.route.summary.as_path.join(' ')}]`;
|
||||||
case 'best_withdrawn': return `${esc(ev.prefix)} has <span class="warn">no route</span> any more`;
|
case 'best_withdrawn': return `${esc(ev.prefix)} has <span class="amber">no route</span> any more`;
|
||||||
case 'lagged': return `<span class="bad">missed ${ev.missed == null ? '' : ev.missed + ' '}events</span> on ${esc(ev.stream)}`;
|
case 'lagged': return `<span class="red">missed ${ev.missed == null ? '' : ev.missed + ' '}events</span> on ${esc(ev.stream)}`;
|
||||||
default: return esc(JSON.stringify(ev));
|
default: return esc(JSON.stringify(ev));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -580,11 +787,18 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
renderRefreshControl();
|
renderRefreshControl();
|
||||||
showLookup();
|
showLookup();
|
||||||
$('lookup').addEventListener('submit', e => {
|
$('lookup').addEventListener('submit', e => { e.preventDefault(); submitLookup(); });
|
||||||
e.preventDefault();
|
|
||||||
setLookup({ q: $('lookup-q').value.trim(), mode: $('lookup-mode').value, peer: $('lookup-peer').value, best: $('lookup-best').checked });
|
|
||||||
});
|
|
||||||
$('lookup-clear').addEventListener('click', e => { e.preventDefault(); setLookup(null); });
|
$('lookup-clear').addEventListener('click', e => { e.preventDefault(); setLookup(null); });
|
||||||
|
$('peers-problems').addEventListener('change', renderPeers);
|
||||||
|
// A peer's name anywhere on the page leads to its routes.
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
const link = e.target.closest('a.peer-link');
|
||||||
|
if (!link) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const l = lookup();
|
||||||
|
setLookup({ q: '', mode: l.mode, peer: link.dataset.peer, best: false });
|
||||||
|
$('received').scrollIntoView();
|
||||||
|
});
|
||||||
for (const id of ['received-pager', 'received-pager-below']) {
|
for (const id of ['received-pager', 'received-pager-below']) {
|
||||||
$(id).addEventListener('click', e => {
|
$(id).addEventListener('click', e => {
|
||||||
const to = e.target.closest('a[data-page]');
|
const to = e.target.closest('a[data-page]');
|
||||||
@@ -592,7 +806,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
e.preventDefault(); setPage(JSON.parse(to.dataset.page));
|
e.preventDefault(); setPage(JSON.parse(to.dataset.page));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
loadAll().catch(e => logEvent(`<span class="bad">${esc(e.message)}</span>`));
|
loadAll().catch(e => logEvent(`<span class="red">${esc(e.message)}</span>`));
|
||||||
if (REFRESH) {
|
if (REFRESH) {
|
||||||
connect();
|
connect();
|
||||||
setInterval(() => loadAll().catch(console.error), REFRESH * 1000);
|
setInterval(() => loadAll().catch(console.error), REFRESH * 1000);
|
||||||
|
|||||||
@@ -25,20 +25,63 @@ Table 1 lists the HTTP endpoints. All endpoints are read-only.
|
|||||||
|
|
||||||
## Status Page
|
## Status Page
|
||||||
|
|
||||||
The status page, `/statusz`, displays the following information:
|
The status page, `/statusz`, opens with a status line that lists what
|
||||||
|
needs attention: neighbors whose sessions are down, with the reason;
|
||||||
|
neighbors that are restarting; the kernel not in step with the routing
|
||||||
|
table, or refusing routes; and RPKI data that is missing or about to
|
||||||
|
expire. When nothing needs attention, the line says so. A second line
|
||||||
|
summarizes the normal state: sessions established, routes installed, and
|
||||||
|
the RPKI state.
|
||||||
|
|
||||||
|
Below the status line, the page displays the following information:
|
||||||
|
|
||||||
|
- A lookup of received routes, by prefix, address, or neighbor name.
|
||||||
- The configuration that was read at startup.
|
- The configuration that was read at startup.
|
||||||
- Each neighbor, with its session state, address families, role and role
|
- Each neighbor, with its session state, time established, next hop,
|
||||||
confirmation, number of accepted routes, rejected announcements, and held
|
session properties, number of accepted routes, rejected announcements,
|
||||||
routes, and last error.
|
and held routes, and its last error. Neighbors that need attention are
|
||||||
- The originated routes, and a lookup of received routes.
|
listed first; the others are grouped by role. The **problems only**
|
||||||
|
option hides the neighbors that need no attention.
|
||||||
- RPKI validation: the cache, the RTR version, and the age of the data.
|
- RPKI validation: the cache, the RTR version, and the age of the data.
|
||||||
|
- The originated routes.
|
||||||
- Kernel route installation: the number of installed routes, the number of
|
- Kernel route installation: the number of installed routes, the number of
|
||||||
routes not installed and the reason, the last error reported by the
|
routes not installed and the reason, the last error reported by the
|
||||||
kernel, and the interfaces.
|
kernel, and the interfaces.
|
||||||
- Rejected routes, by neighbor and by reason.
|
- The received routes that match the lookup, best path first.
|
||||||
|
- Rejected routes, by neighbor, with the number of rejections for each
|
||||||
|
reason, and the most recent rejected routes. Rejections of the same
|
||||||
|
prefix for the same reason by the same AS path are displayed as one row
|
||||||
|
with a count.
|
||||||
- A live event log.
|
- A live event log.
|
||||||
|
|
||||||
|
The page uses color as follows: red indicates a failure that requires
|
||||||
|
action, such as a session that is down; amber indicates a degraded state,
|
||||||
|
such as held or stale routes; green indicates a function that is operating
|
||||||
|
normally, such as the RPKI cache being synchronized; gray indicates an
|
||||||
|
inactive or expected state, such as a passive neighbor waiting for a
|
||||||
|
connection. Normal values are displayed in black.
|
||||||
|
|
||||||
|
Properties that repeat on many rows are displayed as glyphs. Table 2 lists
|
||||||
|
them. Point to a glyph to display its meaning.
|
||||||
|
|
||||||
|
| Glyph | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `v4`, `v6` | The address families of the session. |
|
||||||
|
| `GR` | Graceful restart is in use. |
|
||||||
|
| `ENH` | IPv4 routes are exchanged with IPv6 next hops (RFC 8950). |
|
||||||
|
| `AP↓`, `AP↑` | ADD-PATH: several paths per prefix are received, or sent. |
|
||||||
|
| `2B` | The neighbor supports two-octet AS numbers only. |
|
||||||
|
| `↑`, `↓`, `↔`, `RS`, `RSC`, `iBGP` | The role of the neighbor: provider, customer, peer, route server, route server client, or a router of the local AS. |
|
||||||
|
| `★` | The best path for the prefix. |
|
||||||
|
| `OTC`, `AA`, `AGG`, `AS4`, `OID`, `CL`, `EXT` | Path attributes without a column of their own: Only to Customer, ATOMIC_AGGREGATE, AGGREGATOR, AS4_PATH, ORIGINATOR_ID, CLUSTER_LIST, and extended communities. |
|
||||||
|
| `✓`, `✗`, `–` | RPKI or ASPA validation state: valid, invalid, or not found. |
|
||||||
|
|
||||||
|
Communities for which `community_names` specifies a name are displayed as
|
||||||
|
a badge with that name; the community itself is displayed when you point
|
||||||
|
to the badge. Communities of the local AS that are control communities
|
||||||
|
display their effect in the same way. Well-known communities are
|
||||||
|
displayed by name.
|
||||||
|
|
||||||
To display the name of the holder of an AS number, point to the number. The
|
To display the name of the holder of an AS number, point to the number. The
|
||||||
browser requests the name from [RIPEstat](https://stat.ripe.net/); Ferrix
|
browser requests the name from [RIPEstat](https://stat.ripe.net/); Ferrix
|
||||||
does not send any requests itself. If the browser cannot reach RIPEstat,
|
does not send any requests itself. If the browser cannot reach RIPEstat,
|
||||||
@@ -47,7 +90,7 @@ the page displays "name not available".
|
|||||||
## Route Lookup
|
## Route Lookup
|
||||||
|
|
||||||
The status page provides a lookup of received routes. The HTTP endpoint
|
The status page provides a lookup of received routes. The HTTP endpoint
|
||||||
`/api/routes/received` accepts the same queries. Table 2 lists the query
|
`/api/routes/received` accepts the same queries. Table 3 lists the query
|
||||||
parameters.
|
parameters.
|
||||||
|
|
||||||
| Parameter | Result |
|
| Parameter | Result |
|
||||||
@@ -77,18 +120,17 @@ Ferrix does not store rejected routes in the routing table. For each
|
|||||||
neighbor, it records the number of rejected routes for each reason, and the
|
neighbor, it records the number of rejected routes for each reason, and the
|
||||||
200 most recent rejected routes with all the path attributes they were
|
200 most recent rejected routes with all the path attributes they were
|
||||||
received with. The status page displays this information under "Rejected
|
received with. The status page displays this information under "Rejected
|
||||||
routes"; to display the attributes of a rejected route, select **show** in
|
routes", with the attributes of each rejected route in the same columns as
|
||||||
its row. The HTTP endpoint `/api/routes/rejected` returns the same
|
the received routes. The HTTP endpoint `/api/routes/rejected` returns the
|
||||||
information as JSON. To display the records for one neighbor, add
|
same information as JSON. To display the records for one neighbor, add
|
||||||
`?peer=NAME`.
|
`?peer=NAME`, or select the neighbor's name on the status page.
|
||||||
|
|
||||||
The numbers count rejected announcements, not prefixes. When a neighbor
|
The numbers count rejected announcements, not prefixes. When a neighbor
|
||||||
announces a route again, for example because an attribute changed, and
|
announces a route again, for example because an attribute changed, and
|
||||||
Ferrix rejects it again, the count increases again. Two neighbors that
|
Ferrix rejects it again, the count increases again. Two neighbors that
|
||||||
advertise the same routes can therefore have different counts. The number
|
advertise the same routes can therefore have different counts. The number
|
||||||
of routes currently held, because they are RPKI or ASPA invalid or their
|
of routes currently held, because they are RPKI or ASPA invalid or their
|
||||||
next hop is unreachable, is displayed in the neighbor table as **held
|
next hop is unreachable, is displayed in the neighbor table as **held**.
|
||||||
now**.
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[{"peer":"transit-a","total":3,"by_reason":{"RPKI invalid":2,"bogon prefix":1},
|
[{"peer":"transit-a","total":3,"by_reason":{"RPKI invalid":2,"bogon prefix":1},
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ Top-level statements.
|
|||||||
| `cluster_id` <span class="type">string</span> | `router_id` | Specifies the cluster ID that the router uses as a route reflector, as a dotted-quad IPv4 address. See [Route Reflection](@/docs/guides/passing-routes-on.md#route-reflection). |
|
| `cluster_id` <span class="type">string</span> | `router_id` | Specifies the cluster ID that the router uses as a route reflector, as a dotted-quad IPv4 address. See [Route Reflection](@/docs/guides/passing-routes-on.md#route-reflection). |
|
||||||
| `graceful_restart` <span class="type">[GracefulRestart](#gracefulrestart)</span> | Enabled | Specifies graceful restart for neighbors that have no `graceful_restart` statement. Its `restart_time_seconds` also limits how long Ferrix waits at startup for neighbors to send their routes. |
|
| `graceful_restart` <span class="type">[GracefulRestart](#gracefulrestart)</span> | Enabled | Specifies graceful restart for neighbors that have no `graceful_restart` statement. Its `restart_time_seconds` also limits how long Ferrix waits at startup for neighbors to send their routes. |
|
||||||
| `export_limits` <span class="type">[ExportLimits](#exportlimits)</span> | None | Specifies limits on the routes advertised to neighbors whose role is `PROVIDER`, `PEER`, or `ROUTE_SERVER`, and to EBGP neighbors without a role, for neighbors that have no `export_limits` statement. See [Export Limits](@/docs/guides/passing-routes-on.md#export-limits). |
|
| `export_limits` <span class="type">[ExportLimits](#exportlimits)</span> | None | Specifies limits on the routes advertised to neighbors whose role is `PROVIDER`, `PEER`, or `ROUTE_SERVER`, and to EBGP neighbors without a role, for neighbors that have no `export_limits` statement. See [Export Limits](@/docs/guides/passing-routes-on.md#export-limits). |
|
||||||
|
| `community_names` <span class="type">[CommunityName](#communityname), repeated</span> | None | Specifies names that the status page displays for communities, in place of the numbers. Ferrix does not use the names otherwise. |
|
||||||
|
|
||||||
## Peer
|
## Peer
|
||||||
|
|
||||||
@@ -167,6 +168,16 @@ of the export policy, BGP roles, and communities of the routes. See
|
|||||||
| `prefixes` <span class="type">[PrefixRange](#prefixrange), repeated</span> | All prefixes | Specifies the prefixes that can be advertised to the neighbor. Ferrix does not advertise learned or originated routes for other prefixes. |
|
| `prefixes` <span class="type">[PrefixRange](#prefixrange), repeated</span> | All prefixes | Specifies the prefixes that can be advertised to the neighbor. Ferrix does not advertise learned or originated routes for other prefixes. |
|
||||||
| `max_prefixes` <span class="type">uint32</span> | 0 (no limit) | Specifies the maximum number of routes advertised to the neighbor. If more routes are to be advertised, Ferrix advertises none of them, tears down the session with a Cease NOTIFICATION message (subcode Administrative Shutdown) that states the reason (RFC 9003), and places the neighbor in hold-down for its `import.hold_down_seconds`. |
|
| `max_prefixes` <span class="type">uint32</span> | 0 (no limit) | Specifies the maximum number of routes advertised to the neighbor. If more routes are to be advertised, Ferrix advertises none of them, tears down the session with a Cease NOTIFICATION message (subcode Administrative Shutdown) that states the reason (RFC 9003), and places the neighbor in hold-down for its `import.hold_down_seconds`. |
|
||||||
|
|
||||||
|
## CommunityName
|
||||||
|
|
||||||
|
A name for a community, or for a kind of community, displayed on the status
|
||||||
|
page.
|
||||||
|
|
||||||
|
| Statement | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `community` <span class="type">string</span> | Required | Specifies a standard community (`64500:100`), a large community (`64500:100:2`), or a pattern with `*` for a part left open (`64500:102:*`). For a pattern, the status page displays the name followed by the open part. |
|
||||||
|
| `name` <span class="type">string</span> | Required | Specifies the name to display. |
|
||||||
|
|
||||||
## OriginatedRoute
|
## OriginatedRoute
|
||||||
|
|
||||||
A route that Ferrix originates.
|
A route that Ferrix originates.
|
||||||
|
|||||||
Reference in new issue
Block a user