Status page: tables for the kernel, communities by AS, attributes as text
The kernel section was one run-on line of interfaces and addresses. It is now a small table of the installation state, and a table of interfaces with their IPv4, IPv6 and link-local addresses one per line; ports that are down and have no addresses share one line at the bottom. Communities ran together into one block that wrapped anywhere. Each AS's communities are now a line of their own, kept whole so that they copy, with a hanging indent where a line wraps; pointing at a line names the AS. Where control communities are on (now in /api/status), those of ours that ask for something say what as a tooltip. A rejected route's details were a fold-out that repeated the summary and then dumped the decoded attributes as JSON. Rejected routes now have the same columns as received ones, and both have an "other attributes" column for what the summary had no field for, as text: aggregator, extended communities (route targets and origins by name), AS4_PATH with its sets in braces, originator ID, cluster list, and unknown attributes in hex. The API has them as summary.other; the JSON stays in `attributes`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
89e0c3a3e2
commit
17045a3634
5 files changed
+332
-51
No files matched your search
+202
-3
@@ -2,7 +2,7 @@
|
||||
//! daemon is configured through its configuration file.
|
||||
|
||||
use std::collections::{BTreeMap, BinaryHeap, HashMap};
|
||||
use std::net::IpAddr;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use axum::Json;
|
||||
@@ -11,7 +11,9 @@ use axum::extract::{Query, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use bgp_packet::community;
|
||||
use bgp_packet::message::{NlriNextHop, OriginPathAttribute, PathAttribute};
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, ExtendedCommunity, NlriNextHop, OriginPathAttribute, PathAttribute,
|
||||
};
|
||||
use bgp_primitives::constants::IpVersion;
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::table::{PeerIndex, RibTable, Route, RouteRef};
|
||||
@@ -50,6 +52,9 @@ pub struct StatusView {
|
||||
pub received_routes: usize,
|
||||
/// Prefixes with at least one such route, and so with a best route.
|
||||
pub received_prefixes: usize,
|
||||
/// Whether large communities of our AS ask for things here
|
||||
/// (`control_communities`).
|
||||
pub control_communities: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@@ -289,6 +294,130 @@ pub struct AttrSummary {
|
||||
pub local_pref: Option<u32>,
|
||||
pub communities: Vec<String>,
|
||||
pub large_communities: Vec<String>,
|
||||
/// The attributes not shown above, in the order the route has them,
|
||||
/// as text: `aggregator`, `AS64500 192.0.2.1`.
|
||||
pub other: Vec<OtherAttribute>,
|
||||
}
|
||||
|
||||
/// An attribute of a route that the summary has no field for.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct OtherAttribute {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// An AS path as it is usually written: AS_SETs in braces.
|
||||
fn as_path_text(path: &AsPathAttribute) -> String {
|
||||
let segments = path.segments.iter().map(|segment| {
|
||||
let asns = segment
|
||||
.path
|
||||
.iter()
|
||||
.map(u32::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
if segment.ordered {
|
||||
asns
|
||||
} else {
|
||||
format!("{{{asns}}}")
|
||||
}
|
||||
});
|
||||
segments.collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
|
||||
/// An extended community (RFC 4360) as text. Route targets and route
|
||||
/// origins, of the three kinds of global administrator, by name; the rest
|
||||
/// as their eight octets.
|
||||
fn extended_community_text(community: &ExtendedCommunity) -> String {
|
||||
let b = community.0;
|
||||
let kind = match b[1] {
|
||||
0x02 => "rt",
|
||||
0x03 => "ro",
|
||||
_ => {
|
||||
return format!(
|
||||
"0x{}",
|
||||
b.iter().map(|o| format!("{o:02x}")).collect::<String>()
|
||||
);
|
||||
}
|
||||
};
|
||||
let u16_at = |at: usize| u16::from_be_bytes([b[at], b[at + 1]]);
|
||||
let u32_at = |at: usize| u32::from_be_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]]);
|
||||
match b[0] {
|
||||
0x00 => format!("{kind} {}:{}", u16_at(2), u32_at(4)),
|
||||
0x01 => format!("{kind} {}:{}", Ipv4Addr::from(u32_at(2)), u16_at(6)),
|
||||
0x02 => format!("{kind} {}:{}", u32_at(2), u16_at(6)),
|
||||
_ => format!(
|
||||
"0x{}",
|
||||
b.iter().map(|o| format!("{o:02x}")).collect::<String>()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// The name and value of an attribute the summary has no field for, or
|
||||
/// `None` for one it has.
|
||||
fn other_attribute(attribute: &PathAttribute) -> Option<OtherAttribute> {
|
||||
let (name, value) = match attribute {
|
||||
PathAttribute::Origin(_)
|
||||
| PathAttribute::AsPath(_)
|
||||
| PathAttribute::NextHop(_)
|
||||
| PathAttribute::MultiExitDisc(_)
|
||||
| PathAttribute::LocalPref(_)
|
||||
| PathAttribute::Communities(_)
|
||||
| PathAttribute::LargeCommunities(_)
|
||||
| PathAttribute::MpReachNlri(_)
|
||||
| PathAttribute::MpUnreachNlri(_) => return None,
|
||||
PathAttribute::AtomicAggregate(_) => ("atomic aggregate".to_string(), "set".to_string()),
|
||||
PathAttribute::Aggregator(a) => ("aggregator".to_string(), format!("AS{} {}", a.asn, a.ip)),
|
||||
PathAttribute::ExtendedCommunities(e) => (
|
||||
"extended communities".to_string(),
|
||||
e.extended_communities
|
||||
.iter()
|
||||
.map(extended_community_text)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
),
|
||||
PathAttribute::As4Path(p) => ("AS4_PATH".to_string(), as_path_text(p)),
|
||||
PathAttribute::OnlyToCustomer(asn) => ("only to customer".to_string(), format!("AS{asn}")),
|
||||
PathAttribute::OriginatorId(id) => ("originator ID".to_string(), id.to_string()),
|
||||
PathAttribute::ClusterList(ids) => (
|
||||
"cluster list".to_string(),
|
||||
ids.iter()
|
||||
.map(Ipv4Addr::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" "),
|
||||
),
|
||||
PathAttribute::UnknownPathAttribute {
|
||||
flags,
|
||||
type_code,
|
||||
payload,
|
||||
} => {
|
||||
let kind = [
|
||||
(flags.optional(), "optional"),
|
||||
(flags.transitive(), "transitive"),
|
||||
(flags.partial(), "partial"),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(set, word)| set.then_some(word))
|
||||
.collect::<Vec<_>>();
|
||||
let octets = payload
|
||||
.iter()
|
||||
.map(|o| format!("{o:02x}"))
|
||||
.collect::<String>();
|
||||
(
|
||||
format!("attribute {type_code}"),
|
||||
format!(
|
||||
"{}{} octets{}{octets}",
|
||||
if kind.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{}, ", kind.join(" "))
|
||||
},
|
||||
payload.len(),
|
||||
if payload.is_empty() { "" } else { ": " },
|
||||
),
|
||||
)
|
||||
}
|
||||
};
|
||||
Some(OtherAttribute { name, value })
|
||||
}
|
||||
|
||||
pub fn summarize(attributes: &[PathAttribute], next_hop: Option<&NlriNextHop>) -> AttrSummary {
|
||||
@@ -321,7 +450,7 @@ pub fn summarize(attributes: &[PathAttribute], next_hop: Option<&NlriNextHop>) -
|
||||
PathAttribute::LargeCommunities(l) => {
|
||||
s.large_communities = l.communities.iter().map(community::format_large).collect()
|
||||
}
|
||||
_ => {}
|
||||
other => s.other.extend(other_attribute(other)),
|
||||
}
|
||||
}
|
||||
s
|
||||
@@ -474,6 +603,7 @@ pub async fn status(State(state): State<AppState>) -> Result<Json<StatusView>, U
|
||||
local_routes: state.local_rib.len().await?,
|
||||
received_routes: state.rib.len(),
|
||||
received_prefixes: state.rib.prefix_count(),
|
||||
control_communities: state.info.control_communities,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1259,4 +1389,73 @@ mod tests {
|
||||
assert_eq!(second.prefixes[0], in_order[at + 1]);
|
||||
assert!(second.more_before);
|
||||
}
|
||||
|
||||
/// The attributes without a field in the summary are listed as text,
|
||||
/// in the order the route has them.
|
||||
#[test]
|
||||
fn other_attributes_as_text() {
|
||||
use bgp_packet::message::{
|
||||
AggregatorPathAttribute, AsPathSegment, AtomicAggregatePathAttribute,
|
||||
ExtendedCommunitiesPathAttribute, PathAttributeFlags,
|
||||
};
|
||||
let attributes = [
|
||||
PathAttribute::AsPath(AsPathAttribute { segments: vec![] }),
|
||||
PathAttribute::AtomicAggregate(AtomicAggregatePathAttribute {}),
|
||||
PathAttribute::Aggregator(AggregatorPathAttribute {
|
||||
asn: 64500,
|
||||
ip: Ipv4Addr::new(192, 0, 2, 1),
|
||||
}),
|
||||
PathAttribute::ExtendedCommunities(ExtendedCommunitiesPathAttribute {
|
||||
extended_communities: vec![
|
||||
ExtendedCommunity([0x00, 0x02, 0xfd, 0xe8, 0, 0, 0, 100]),
|
||||
ExtendedCommunity([0x01, 0x03, 192, 0, 2, 1, 0, 7]),
|
||||
ExtendedCommunity([0x02, 0x02, 0, 3, 0x0d, 0x44, 0, 5]),
|
||||
ExtendedCommunity([0x03, 0x0c, 1, 2, 3, 4, 5, 6]),
|
||||
],
|
||||
}),
|
||||
PathAttribute::As4Path(AsPathAttribute {
|
||||
segments: vec![
|
||||
AsPathSegment {
|
||||
ordered: true,
|
||||
path: vec![64500, 64501],
|
||||
},
|
||||
AsPathSegment {
|
||||
ordered: false,
|
||||
path: vec![64502, 64503],
|
||||
},
|
||||
],
|
||||
}),
|
||||
PathAttribute::OnlyToCustomer(64500),
|
||||
PathAttribute::OriginatorId(Ipv4Addr::new(192, 0, 2, 9)),
|
||||
PathAttribute::ClusterList(vec![
|
||||
Ipv4Addr::new(10, 0, 0, 1),
|
||||
Ipv4Addr::new(10, 0, 0, 2),
|
||||
]),
|
||||
PathAttribute::UnknownPathAttribute {
|
||||
flags: PathAttributeFlags::new(true, true, false, false),
|
||||
type_code: 99,
|
||||
payload: vec![0xab, 0x01],
|
||||
},
|
||||
];
|
||||
let other: Vec<(String, String)> = summarize(&attributes, None)
|
||||
.other
|
||||
.into_iter()
|
||||
.map(|a| (a.name, a.value))
|
||||
.collect();
|
||||
let expected = [
|
||||
("atomic aggregate", "set"),
|
||||
("aggregator", "AS64500 192.0.2.1"),
|
||||
(
|
||||
"extended communities",
|
||||
"rt 65000:100, ro 192.0.2.1:7, rt 200004:5, 0x030c010203040506",
|
||||
),
|
||||
("AS4_PATH", "64500 64501 {64502 64503}"),
|
||||
("only to customer", "AS64500"),
|
||||
("originator ID", "192.0.2.9"),
|
||||
("cluster list", "10.0.0.1 10.0.0.2"),
|
||||
("attribute 99", "optional transitive, 2 octets: ab01"),
|
||||
]
|
||||
.map(|(name, value)| (name.to_string(), value.to_string()));
|
||||
assert_eq!(other, expected);
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,9 @@ pub struct RouterInfo {
|
||||
/// The configuration file as read at startup (path and raw text), shown
|
||||
/// on the status page. `None` when the server was configured in code.
|
||||
pub config: Option<(String, String)>,
|
||||
/// Whether large communities of our AS ask for things here
|
||||
/// (`control_communities`), so that the status page can say what.
|
||||
pub control_communities: bool,
|
||||
}
|
||||
|
||||
impl RouterInfo {
|
||||
@@ -58,9 +61,16 @@ impl RouterInfo {
|
||||
started_at: SystemTime::now(),
|
||||
hostname: hostname(),
|
||||
config: None,
|
||||
control_communities: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Records whether control communities are acted on.
|
||||
pub fn with_control_communities(mut self, on: bool) -> Self {
|
||||
self.control_communities = on;
|
||||
self
|
||||
}
|
||||
|
||||
/// Records the configuration file so the status page can show it.
|
||||
pub fn with_config(mut self, path: impl Into<String>, text: impl Into<String>) -> Self {
|
||||
self.config = Some((path.into(), text.into()));
|
||||
|
||||
+112
-44
@@ -30,13 +30,19 @@ table.data > tbody > tr:hover > td { background: #f3f3ff; }
|
||||
.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; }
|
||||
.dim { color: #666; }
|
||||
.nowrap { white-space: nowrap; }
|
||||
table.data td.path { min-width: 12em; }
|
||||
#event-log { font-family: monospace; font-size: 12px; max-height: 22em; overflow-y: auto; }
|
||||
#event-log div { white-space: pre-wrap; }
|
||||
/* Communities: one line per AS that defines them, each kept whole. */
|
||||
.comm { white-space: nowrap; margin-right: 0.7em; }
|
||||
.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 { min-width: 30em; }
|
||||
table.kv th { background: #eef; font-weight: normal; }
|
||||
table.data tr.dim td { color: #666; }
|
||||
details { clear: both; margin: 8px 0 0; }
|
||||
details.refused-detail { margin: 0; }
|
||||
details.refused-detail summary { cursor: pointer; }
|
||||
details.refused-detail table td { padding: 1px 8px 1px 0; border: 0; }
|
||||
details.refused-detail pre { max-height: 20em; overflow: auto; margin: 4px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
summary { cursor: pointer; color: #00c; }
|
||||
summary:hover { text-decoration: underline; }
|
||||
#config-text { border: 1px solid #99a; padding: 4px 6px; margin: 4px 0; font-size: 12px; overflow-x: auto; }
|
||||
@@ -102,7 +108,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
</form>
|
||||
<div class="pager" id="received-pager"></div>
|
||||
<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>RPKI</th><th>ASPA</th><th>best</th></tr></thead>
|
||||
<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>
|
||||
<tbody id="received-body"></tbody>
|
||||
</table>
|
||||
<div class="pager" id="received-pager-below"></div>
|
||||
@@ -110,7 +116,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
<h1 id="refused">Rejected routes (<span id="refused-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>
|
||||
<table class="data">
|
||||
<thead><tr><th>peer</th><th>prefix</th><th>AS path</th><th>reason</th><th>when</th><th>attributes</th></tr></thead>
|
||||
<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>
|
||||
<tbody id="refused-body"></tbody>
|
||||
</table>
|
||||
|
||||
@@ -155,8 +161,67 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
if (state.status) $('s-uptime').textContent = fmtDur(t - state.status.started_at_unix);
|
||||
}
|
||||
const fmtTime = unix => new Date(unix * 1000).toLocaleTimeString();
|
||||
const commList = (list, named) => list.length ? list.map(c =>
|
||||
named && WELL_KNOWN[c] ? `<span title="${esc(c)}">${WELL_KNOWN[c]}</span>` : esc(c)).join(' ') : DASH;
|
||||
const fmtCount = n => Number(n).toLocaleString('en-US');
|
||||
|
||||
// What a community of ours asks for where control communities are on
|
||||
// (control_communities), or ''. They are acted on where a route leaves
|
||||
// our AS.
|
||||
const TIMES = { 1: 'once', 2: 'twice', 3: 'three times' };
|
||||
function controlMeaning(c, large) {
|
||||
const s = state.status;
|
||||
if (!s || !s.control_communities) return '';
|
||||
const L = s.local_asn, p = c.split(':').map(Number);
|
||||
if (!large) {
|
||||
const [high, low] = p;
|
||||
if (high === 0) return low === L ? 'to no other network' : `not to AS${low}`;
|
||||
if (high === L) return low === L ? 'to every network' : `only to AS${low} (and others named so)`;
|
||||
return '';
|
||||
}
|
||||
const [admin, fn, arg] = p;
|
||||
if (admin !== L) return '';
|
||||
if (fn === 0) return arg ? `not to AS${arg}` : 'to no other network';
|
||||
if (fn === 1) return `only to AS${arg} (and others named so)`;
|
||||
if (fn >= 11 && fn <= 13) return `our AS ${TIMES[fn - 10]} more on the path to ${arg ? `AS${arg}` : 'every network'}`;
|
||||
if (fn === 20) return `not to group ${arg}`;
|
||||
if (fn === 21) return `only to group ${arg} (and others named so)`;
|
||||
if (fn >= 31 && fn <= 33) return `our AS ${TIMES[fn - 30]} more on the path to group ${arg}`;
|
||||
return '';
|
||||
}
|
||||
|
||||
// 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
|
||||
// names its AS. Well-known ones by name, and ours that ask for something
|
||||
// with what they ask.
|
||||
function commList(list, large) {
|
||||
if (!list.length) return DASH;
|
||||
const owners = new Map();
|
||||
for (const c of list) {
|
||||
const owner = c.split(':')[0];
|
||||
if (!owners.has(owner)) owners.set(owner, []);
|
||||
owners.get(owner).push(c);
|
||||
}
|
||||
const local = state.status ? String(state.status.local_asn) : null;
|
||||
const lines = [...owners].map(([owner, cs]) => {
|
||||
const items = cs.map(c => {
|
||||
const name = !large && WELL_KNOWN[c];
|
||||
if (name) return `<span class="comm" title="${esc(c)}">${name}</span>`;
|
||||
const meaning = controlMeaning(c, large);
|
||||
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">${esc(c)}</span>`;
|
||||
}).join(' ');
|
||||
// Not for our own AS, nor for 0 and 65535, which are nobody's.
|
||||
const named = owner !== local && owner !== '0' && owner !== '65535';
|
||||
return named ? `<div class="asn" data-asn="${esc(owner)}">${items}</div>` : `<div>${items}</div>`;
|
||||
});
|
||||
return `<div class="comms mono">${lines.join('')}</div>`;
|
||||
}
|
||||
|
||||
// The attributes that have no column of their own.
|
||||
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('')
|
||||
: DASH;
|
||||
|
||||
// An AS number, whose holder's name is shown when it is pointed at.
|
||||
const asn = n => `<span class="asn" data-asn="${esc(n)}">${esc(n)}</span>`;
|
||||
const asPath = p => p.length ? p.map(asn).join(' ') : '<span class="dim">(empty)</span>';
|
||||
@@ -285,8 +350,8 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
const local = state.status ? state.status.local_asn : '?';
|
||||
setHTML(b, state.local.map(r => `<tr>
|
||||
<td class="mono">${esc(r.prefix)}</td><td>${esc(r.origin)}</td>
|
||||
<td class="mono">${asPath(Array(1 + r.prepend).fill(local))}</td><td>${r.med ?? DASH}</td>
|
||||
<td class="mono">${commList(r.communities, true)}</td><td class="mono">${commList(r.large_communities)}</td>
|
||||
<td class="mono path">${asPath(Array(1 + r.prepend).fill(local))}</td><td>${r.med ?? DASH}</td>
|
||||
<td>${commList(r.communities)}</td><td>${commList(r.large_communities, true)}</td>
|
||||
<td>${age(r.announced_at_unix)}</td></tr>`).join(''));
|
||||
}
|
||||
|
||||
@@ -301,12 +366,12 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
: `${shown}${st ? `, for ${st.received_prefixes} prefixes` : ''}`;
|
||||
renderPager();
|
||||
if (!state.received.length) {
|
||||
setHTML(b, `<tr><td colspan="12" class="dim">${looking ? 'No route matches.' : 'No routes received from peers.'}</td></tr>`); return;
|
||||
setHTML(b, `<tr><td colspan="13" 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>
|
||||
<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">${asPath(s.as_path)}</td>
|
||||
<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 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">${commList(s.communities, true)}</td><td class="mono">${commList(s.large_communities)}</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>${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(''));
|
||||
}
|
||||
@@ -331,32 +396,33 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
const k = state.kernel, b = $('kernel-body');
|
||||
if (!k) return;
|
||||
if (!k.enabled) { setHTML(b, '<span class="dim">Not configured: the best routes are not put in the kernel, and this machine does not forward by them.</span>'); return; }
|
||||
const unresolved = Object.entries(k.unresolved).map(([why, n]) => `${n} ${esc(why)}`).join(', ');
|
||||
const interfaces = k.interfaces.map(i =>
|
||||
`<span class="mono">${esc(i.name)}</span> <span class="${i.up ? 'ok' : 'bad'}">${i.up ? 'up' : 'down'}</span>` +
|
||||
(i.addresses.length ? ` <span class="mono dim">${i.addresses.map(esc).join(' ')}</span>` : '')).join(' · ');
|
||||
setHTML(b, `<span class="${k.synced ? 'ok' : 'warn'}">${k.synced ? 'In step' : 'Catching up'}</span> with the routing table: ` +
|
||||
`<b>${k.installed}</b> routes installed <span class="dim">(table ${k.table}, metric ${k.metric}, proto bgp)</span>` +
|
||||
(k.kept ? `; <span class="warn">${k.kept} kept from before the restart</span>, not announced again yet` : '') +
|
||||
(unresolved ? `; <span class="warn">not installed:</span> ${unresolved}` : '') +
|
||||
(k.refused ? `; <span class="bad">${k.refused} refused by the kernel</span>` : '') + '.' +
|
||||
(k.last_error ? `<br><span class="bad">Last error</span> (${k.errors} in all): ${esc(k.last_error)}` : '') +
|
||||
(interfaces ? `<br>Interfaces: ${interfaces}` : ''));
|
||||
}
|
||||
|
||||
// Everything a refused route came with, folded away: the fields of the
|
||||
// received routes' table, then the attributes as decoded.
|
||||
function refusedDetail(r) {
|
||||
const s = r.summary;
|
||||
const field = (name, value) => `<tr><td class="dim">${name}</td><td class="mono">${value}</td></tr>`;
|
||||
return `<details class="refused-detail"><summary>show</summary><table>` +
|
||||
field('next hop', s.next_hop ? esc(s.next_hop) : DASH) +
|
||||
field('origin', s.origin ? esc(s.origin) : DASH) +
|
||||
field('MED', s.med ?? DASH) +
|
||||
field('local pref', s.local_pref ?? DASH) +
|
||||
field('communities', commList(s.communities, true)) +
|
||||
field('large communities', commList(s.large_communities)) +
|
||||
`</table><pre class="mono">${esc(r.attributes.map(a => JSON.stringify(a)).join('\n'))}</pre></details>`;
|
||||
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 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('routes installed', `<b>${fmtCount(k.installed)}</b>`) +
|
||||
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`) : '') +
|
||||
(unresolved ? row('not installed', `<span class="warn">${unresolved}</span>`) : '') +
|
||||
(k.refused ? row('refused by the kernel', `<span class="bad">${fmtCount(k.refused)}</span>`) : '') +
|
||||
(k.last_error ? row('last error', `<span class="bad">${esc(k.last_error)}</span> (${fmtCount(k.errors)} errors in all)`) : '') +
|
||||
'</table>';
|
||||
// Interfaces that are up or have addresses get a row each; the others
|
||||
// (unused ports, mostly) one line between them.
|
||||
const v4 = a => !a.includes(':'), linkLocal = a => /^fe80:/i.test(a);
|
||||
const lines = list => list.length ? list.map(esc).join('<br>') : DASH;
|
||||
const listed = k.interfaces.filter(i => i.up || i.addresses.length);
|
||||
const idle = k.interfaces.filter(i => !i.up && !i.addresses.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>' +
|
||||
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 class="mono">${lines(i.addresses.filter(v4))}</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('') +
|
||||
(idle.length ? `<tr class="dim"><td colspan="5">Down and without addresses: <span class="mono">${idle.map(i => esc(i.name)).join(' ')}</span></td></tr>` : '') +
|
||||
'</tbody></table>';
|
||||
setHTML(b, summary + interfaces);
|
||||
}
|
||||
|
||||
// For each peer a line that says how many and why, then its latest few.
|
||||
@@ -366,16 +432,18 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
const total = state.refused.reduce((n, p) => n + p.total, 0);
|
||||
const announcements = n => `${n} announcement${n === 1 ? '' : 's'}`;
|
||||
$('refused-count').textContent = total ? `${announcements(total)} from ${state.refused.length} peer${state.refused.length === 1 ? '' : 's'}` : '0';
|
||||
if (!state.refused.length) { setHTML(b, '<tr><td colspan="6" class="dim">Nothing a peer announced has been rejected.</td></tr>'); return; }
|
||||
if (!state.refused.length) { setHTML(b, '<tr><td colspan="12" class="dim">Nothing a peer announced has been rejected.</td></tr>'); return; }
|
||||
setHTML(b, state.refused.map(p => {
|
||||
const why = Object.entries(p.by_reason).sort((a, c) => c[1] - a[1]).map(([reason, n]) => `${n} ${esc(reason)}`).join(', ');
|
||||
const more = p.recent.length > REFUSED_SHOWN
|
||||
? ` <span class="dim">Latest ${REFUSED_SHOWN} shown; <a href="/api/routes/rejected?peer=${encodeURIComponent(p.peer)}">the latest ${p.recent.length} as json</a>.</span>` : '';
|
||||
return `<tr><td colspan="6"><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>` +
|
||||
p.recent.slice(0, REFUSED_SHOWN).map(r => `<tr><td></td>
|
||||
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>` +
|
||||
p.recent.slice(0, REFUSED_SHOWN).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">${asPath(r.as_path)}</td><td>${esc(r.reason)}</td><td>${age(r.at_unix)} ago</td>
|
||||
<td>${refusedDetail(r)}</td></tr>`).join('');
|
||||
<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">${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('');
|
||||
}).join(''));
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user