Status page: routes in order of selection, lookup by origin AS
Received routes are listed from the best down: the RIB gains ranked_routes, which runs selection again without the winners, so the second route is the one that would be installed were the best withdrawn. The API gives each route its rank, and the page shows it as a numbered badge after the star. The lookup box moves to the received-routes table, which is what it filters, and takes an AS number: a new `origin` query parameter finds every route whose path ends in that AS, with an empty path counting as the local AS. Long cells are clamped to a few lines with a control to open them, and an AS repeated along a path (prepending) is shown once with a ×N badge. Peer names are bold and dark, not underlined links, so a row can be found by its name among the addresses and numbers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
1ce7eef27e
commit
35ce2e2e57
9 files changed
+370
-66
No files matched your search
@@ -63,7 +63,22 @@ table.data tr.dim td { color: #666; }
|
||||
.comms { min-width: 26em; }
|
||||
table.kv th { background: #eef; font-weight: normal; position: static; }
|
||||
table.data td.path { min-width: 12em; }
|
||||
/* An AS repeated along the path (prepended): once, with how many times. */
|
||||
.g.x { font-family: monospace; background: #eef4ff; border-color: #88a; margin-left: 1px; cursor: help; }
|
||||
/* A cell with more in it than a few lines: clamped until opened. */
|
||||
.clamp { max-height: 4.4em; overflow: hidden; }
|
||||
.clamp.tall:not(.open) { -webkit-mask-image: linear-gradient(#000 60%, transparent); mask-image: linear-gradient(#000 60%, transparent); }
|
||||
.clamp.open { max-height: none; }
|
||||
.clamp-more { cursor: pointer; display: none; margin-top: 2px; }
|
||||
.tall + .clamp-more { display: inline-block; }
|
||||
table.data td.name { white-space: nowrap; }
|
||||
/* Peer names are what a row is found by: bold, dark, and not underlined
|
||||
until pointed at, so that they stand out from the addresses and numbers
|
||||
around them rather than joining the blue of every other link. */
|
||||
table.data td.name a.peer-link { font-weight: bold; color: #112; text-decoration: none; font-size: 13.5px; }
|
||||
table.data td.name a.peer-link:hover { text-decoration: underline; color: #00c; }
|
||||
/* Where the route is in path selection: the best, or the Nth to take over. */
|
||||
.g.rank { font-family: sans-serif; color: #446; background: #f3f3f8; min-width: 1em; text-align: center; }
|
||||
/* 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; }
|
||||
@@ -93,6 +108,7 @@ form.lookup input[type=text] { font-family: monospace; }
|
||||
<div class="lefthand">
|
||||
<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>
|
||||
Started <span id="s-started">...</span>, up <span id="s-uptime">...</span><br>
|
||||
<span class="dim">On this page: <a href="#peers">peers</a> · <a href="#rpki">RPKI</a> · <a href="#local">originated</a> · <a href="#kernel">kernel</a> · <a href="#received">received, and looking routes up</a> · <a href="#rejected">rejected</a> · <a href="#events">events</a></span><br>
|
||||
</div>
|
||||
<div class="righthand">
|
||||
<span id="refresh-ctl"></span><br>
|
||||
@@ -102,19 +118,6 @@ JSON: <a href="/api/status">status</a>, <a href="/api/peers">peers</a>, <a href=
|
||||
|
||||
<div id="strip" style="clear: both">...</div>
|
||||
|
||||
<form class="lookup" id="lookup">
|
||||
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">
|
||||
<option value="exact">routes for this prefix or address</option>
|
||||
<option value="within">this prefix and everything inside it</option>
|
||||
</select>
|
||||
from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
<label><input type="checkbox" id="lookup-best"> best only</label>
|
||||
<button type="submit">look up</button>
|
||||
<a href="#" id="lookup-clear">show all</a>
|
||||
<span id="lookup-note"></span>
|
||||
</form>
|
||||
|
||||
<details id="config-zippy" hidden>
|
||||
<summary>Configuration <span class="dim" id="config-path"></span></summary>
|
||||
<pre id="config-text"></pre>
|
||||
@@ -143,6 +146,18 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
<div id="kernel-body">...</div>
|
||||
|
||||
<h1 id="received">Received routes (<span id="received-count">...</span>)</h1>
|
||||
<form class="lookup" id="lookup">
|
||||
Look up <input type="text" id="lookup-q" size="30" placeholder="prefix, address, AS number, or peer" title="A prefix; an address, to find every prefix that covers it; an AS number, to find every route it originates; or a peer's name">
|
||||
<select id="lookup-mode" title="What to find for a prefix">
|
||||
<option value="exact">routes for this prefix or address</option>
|
||||
<option value="within">this prefix and everything inside it</option>
|
||||
</select>
|
||||
from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
<label><input type="checkbox" id="lookup-best"> best only</label>
|
||||
<button type="submit">look up</button>
|
||||
<a href="#" id="lookup-clear">show all</a>
|
||||
<span id="lookup-note"></span>
|
||||
</form>
|
||||
<div class="pager" id="received-pager"></div>
|
||||
<table class="data">
|
||||
<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>
|
||||
@@ -297,7 +312,30 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
|
||||
// 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>';
|
||||
// A path, with an AS that repeats (prepending) once, and how many times.
|
||||
function asPath(p) {
|
||||
if (!p.length) return '<span class="dim">(empty)</span>';
|
||||
const runs = [];
|
||||
for (const n of p) {
|
||||
const last = runs[runs.length - 1];
|
||||
if (last && last.asn === n) last.times++; else runs.push({ asn: n, times: 1 });
|
||||
}
|
||||
return runs.map(r => r.times === 1 ? asn(r.asn)
|
||||
: `<span class="nowrap">${asn(r.asn)}${glyph(`×${r.times}`, `AS${r.asn} ${r.times} times in a row: prepended ${r.times - 1} times`, 'x')}</span>`).join(' ');
|
||||
}
|
||||
// Wraps what can run to many lines: shown clamped, with a way to open
|
||||
// it, if it does. See markTall.
|
||||
const clamp = html => `<div class="clamp">${html}</div><span class="g dim clamp-more">▾ more</span>`;
|
||||
// After a render: finds the clamped cells with more than fits.
|
||||
function markTall(root) {
|
||||
for (const c of root.querySelectorAll('.clamp')) c.classList.toggle('tall', c.scrollHeight > c.clientHeight + 1);
|
||||
}
|
||||
function toggleClamp(cell) {
|
||||
const c = cell.querySelector('.clamp.tall'), more = cell.querySelector('.clamp-more');
|
||||
if (!c) return;
|
||||
const open = c.classList.toggle('open');
|
||||
more.textContent = open ? '▴ less' : '▾ more';
|
||||
}
|
||||
|
||||
async function getJSON(url) {
|
||||
const r = await fetch(url);
|
||||
@@ -448,7 +486,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
// ---- looking routes up -------------------------------------------------
|
||||
// The query lives in the page's address, so that it survives a reload and
|
||||
// auto-refresh, and can be passed on.
|
||||
const QUERY_KEYS = ['q', 'mode', 'peer', 'best'];
|
||||
const QUERY_KEYS = ['q', 'mode', 'peer', 'origin', 'best'];
|
||||
// Which page of the listing is shown lives there too: the prefix the
|
||||
// page comes after, or the one it comes before.
|
||||
const PAGE_KEYS = ['after', 'before'];
|
||||
@@ -456,7 +494,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
const PAGE_ROUTES = 100;
|
||||
function lookup() {
|
||||
const here = new URLSearchParams(location.search);
|
||||
return { q: (here.get('q') || '').trim(), mode: here.get('mode') || 'exact', peer: here.get('peer') || '', best: here.get('best') === '1' };
|
||||
return { q: (here.get('q') || '').trim(), mode: here.get('mode') || 'exact', peer: here.get('peer') || '', origin: here.get('origin') || '', best: here.get('best') === '1' };
|
||||
}
|
||||
function pagePlace() {
|
||||
const here = new URLSearchParams(location.search);
|
||||
@@ -472,12 +510,13 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
renderRefreshControl();
|
||||
loadReceived().catch(console.error);
|
||||
}
|
||||
function isLookup() { const l = lookup(); return !!(l.q || l.peer || l.best); }
|
||||
function isLookup() { const l = lookup(); return !!(l.q || l.peer || l.origin || l.best); }
|
||||
// What to ask the API for: an address finds every prefix that covers it.
|
||||
function receivedUrl() {
|
||||
const l = lookup(), ask = new URLSearchParams();
|
||||
if (l.q) ask.set(l.mode === 'within' ? 'within' : l.q.includes('/') ? 'prefix' : 'address', l.q);
|
||||
if (l.peer) ask.set('peer', l.peer);
|
||||
if (l.origin) ask.set('origin', l.origin);
|
||||
if (l.best) ask.set('best', 'true');
|
||||
const place = pagePlace();
|
||||
if (place.after) ask.set('after', place.after); else if (place.before) ask.set('before', place.before);
|
||||
@@ -494,7 +533,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
}
|
||||
function showLookup() {
|
||||
const l = lookup();
|
||||
$('lookup-q').value = l.q; $('lookup-mode').value = l.mode; $('lookup-best').checked = l.best;
|
||||
$('lookup-q').value = l.origin ? `AS${l.origin}` : l.q; $('lookup-mode').value = l.mode; $('lookup-best').checked = l.best;
|
||||
renderPeerChoices();
|
||||
}
|
||||
function setLookup(l) {
|
||||
@@ -505,6 +544,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
if (l.q) url.searchParams.set('q', l.q);
|
||||
if (l.q && l.mode === 'within') url.searchParams.set('mode', 'within');
|
||||
if (l.peer) url.searchParams.set('peer', l.peer);
|
||||
if (l.origin) url.searchParams.set('origin', l.origin);
|
||||
if (l.best) url.searchParams.set('best', '1');
|
||||
}
|
||||
history.replaceState(null, '', url.pathname + url.search + '#received');
|
||||
@@ -513,11 +553,14 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
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.
|
||||
// to look at, not a prefix. A number, with or without AS in front, is
|
||||
// an AS: the routes it originates.
|
||||
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 });
|
||||
const asn = /^(?:AS)?(\d+)$/i.exec(typed);
|
||||
setLookup({ q: isPeer || asn ? '' : typed, mode: $('lookup-mode').value, peer: isPeer ? typed : $('lookup-peer').value,
|
||||
origin: asn ? asn[1] : '', best: $('lookup-best').checked });
|
||||
}
|
||||
|
||||
function renderLocal() {
|
||||
@@ -539,6 +582,14 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
const [mark, cls] = VALIDITY[value] || [value, ''];
|
||||
return `<span class="${cls}" title="${esc(what)}: ${esc(value)}">${mark}</span>`;
|
||||
}
|
||||
// Where a route stands in path selection: the best, or the one that
|
||||
// would take over were those ranked before it gone.
|
||||
const ORDINAL = n => n + (n % 10 === 2 && n % 100 !== 12 ? 'nd' : n % 10 === 3 && n % 100 !== 13 ? 'rd' : 'th');
|
||||
function rankGlyph(r) {
|
||||
if (r.best) return glyph('★', 'The best route for this prefix: the one chosen, installed and passed on', 'green');
|
||||
if (!r.rank) return '';
|
||||
return glyph(String(r.rank), `${ORDINAL(r.rank)} in path selection: the route that would be chosen were the ${r.rank - 1} before it gone`, 'rank');
|
||||
}
|
||||
function renderReceived() {
|
||||
const b = $('received-body'), st = state.status, looking = isLookup();
|
||||
const page = state.receivedPage, paged = !!(page.next_after || page.prev_before);
|
||||
@@ -553,14 +604,15 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
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 class="${r.best ? 'best' : ''}">
|
||||
<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="nowrap">${rankGlyph(r)}${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">${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 path">${clamp(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>${clamp(commList(s.communities))}</td><td>${clamp(commList(s.large_communities, true))}</td><td>${otherList(s.other)}</td>
|
||||
<td class="nowrap">${validity('RPKI', r.rpki)} ${validity('ASPA', r.aspa)}</td></tr>`; }).join(''));
|
||||
markTall(b);
|
||||
}
|
||||
|
||||
const RPKI_STATE_CLASS = { synchronised: 'green', synchronising: 'amber', connecting: 'red' };
|
||||
@@ -648,12 +700,13 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
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 path">${asPath(r.as_path)}</td><td>${esc(r.reason)}</td>
|
||||
<td class="mono path">${clamp(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>${commList(s.communities)}</td><td>${commList(s.large_communities, true)}</td><td>${otherList(s.other)}</td></tr>`; }).join('');
|
||||
<td>${clamp(commList(s.communities))}</td><td>${clamp(commList(s.large_communities, true))}</td><td>${otherList(s.other)}</td></tr>`; }).join('');
|
||||
}).join(''));
|
||||
markTall(b);
|
||||
}
|
||||
|
||||
// ---- data loading ------------------------------------------------------
|
||||
@@ -790,6 +843,8 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
$('lookup').addEventListener('submit', e => { e.preventDefault(); submitLookup(); });
|
||||
$('lookup-clear').addEventListener('click', e => { e.preventDefault(); setLookup(null); });
|
||||
$('peers-problems').addEventListener('change', renderPeers);
|
||||
document.addEventListener('click', e => { if (e.target.closest('.clamp-more')) toggleClamp(e.target.closest('td')); });
|
||||
document.addEventListener('dblclick', e => { const td = e.target.closest('td'); if (td && td.querySelector('.clamp')) { toggleClamp(td); getSelection().removeAllRanges(); } });
|
||||
// A peer's name anywhere on the page leads to its routes.
|
||||
document.addEventListener('click', e => {
|
||||
const link = e.target.closest('a.peer-link');
|
||||
|
||||
Reference in new issue
Block a user