policy, config, web: the TCP gauges named once, one revalidator, one peer-naming rule
A code-quality pass over policy, configuration and the status API. The twenty TCP gauges were spelled out in the struct, the constructor, the removal, the sampler in the web crate and the status view; one macro names each once with its help text, `PeerTcp::set` fills them from a connection, and the view is a `From`. Every metric and the list that forces them at start come from one declaration, with a test that every declared family is registered. The two revalidators, for next hops and for RPKI, share the task body that scans the table off the sessions' threads and fans the findings out to every session; the RPKI scan judges a route with the same `Checks` the import path uses. Which peer a query names is one rule for the received, rejected and TCP endpoints, so `/api/tcp` takes a name or an address like the others. The config parser says proto3's "zero is unset" in three helpers instead of a dozen shapes, and `parse_peer` is three named parts shorter. The policy's test-only next-hop flag is named for what it does, `accept_own_host_next_hop`; the "listed, or else ours" prefix rule and the mark-then-prefer step each exist once. The status page derives its spanning rows from the table's header, shares one key-value grid and one detail-row rule, and defines its helpers before their use. The web crate no longer re-exports views nothing uses, and the TCP endpoint sits above the test module. Testing: the TCP view's arithmetic (a share of no busy time is zero, not NaN), the peer-naming rule, the RPKI scan finding only what a change can have made invalid, and `/api/tcp` by address, by name and for an unknown peer. The server and web tests, the HTTP, roles, RPKI and import-policy integration tests, the docs build and every lab phase pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
4cddd6fb04
commit
db8edaae8e
13 files changed
+1037
-879
No files matched your search
@@ -68,12 +68,15 @@ table.data th { background: var(--head); font-weight: bold; position: sticky; to
|
||||
table.data > tbody > tr:hover > td { background: var(--hover); }
|
||||
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: var(--band); font-weight: bold; padding: 3px 6px; }
|
||||
table.data tr.tcp-row > td { background: var(--band); padding: 6px 10px 8px 28px; }
|
||||
/* A row of detail under a row of the table: a session's TCP connection, a
|
||||
route in full. The route's is indented past the rank column. */
|
||||
table.data tr.detail > td { background: var(--band); padding: 6px 10px 8px 28px; }
|
||||
table.data tr.route-detail > td { padding-left: 40px; }
|
||||
table.data td.attrs { cursor: pointer; }
|
||||
table.data td.attrs:hover { background: var(--hover); }
|
||||
table.data tr.route-detail > td { background: var(--band); padding: 6px 10px 8px 40px; }
|
||||
.tcp-grid { display: grid; grid-template-columns: max-content 1fr; gap: 1px 16px; font-size: 12px; }
|
||||
.tcp-grid > div:nth-child(odd) { color: var(--fg-soft); white-space: nowrap; }
|
||||
/* Names and values, two columns, as the detail rows show them. */
|
||||
.kv-grid { display: grid; grid-template-columns: max-content 1fr; gap: 1px 16px; font-size: 12px; }
|
||||
.kv-grid > div:nth-child(odd) { color: var(--fg-soft); white-space: nowrap; }
|
||||
a.tcp-link { color: var(--dim); font-size: 11.5px; text-decoration: none; border: 1px solid var(--line); border-radius: 3px; padding: 0 4px; margin-left: 4px; }
|
||||
a.tcp-link:hover, a.tcp-link.open { color: var(--link); border-color: var(--link); }
|
||||
table.data tr.best > td { background: var(--best-bg); }
|
||||
@@ -276,6 +279,17 @@ 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 fmtWhen = unix => new Date(unix * 1000).toLocaleString();
|
||||
const fmtOctets = n => n >= 1048576 ? `${(n / 1048576).toFixed(1)} MiB` : n >= 1024 ? `${(n / 1024).toFixed(1)} KiB` : `${n} B`;
|
||||
const fmtBits = n => { const b = n * 8; return b >= 1e9 ? `${(b / 1e9).toFixed(2)} Gbit/s` : b >= 1e6 ? `${(b / 1e6).toFixed(1)} Mbit/s` : `${(b / 1e3).toFixed(0)} kbit/s`; };
|
||||
const fmtMs = n => n >= 1000 ? `${(n / 1000).toFixed(1)} s` : `${n.toFixed(n < 10 ? 2 : 0)} ms`;
|
||||
const ago = ms => ms >= 60000 ? `${Math.round(ms / 60000)}m` : ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
|
||||
// A cell across every column of the table that `body` is the body of,
|
||||
// and a row of one: for a heading, a note or a detail row. The width is
|
||||
// the table's header's, so a column added there is covered here.
|
||||
const fullCell = (body, html, cls = '') => `<td colspan="${body.closest('table').querySelectorAll('thead th').length}"${cls ? ` class="${cls}"` : ''}>${html}</td>`;
|
||||
const fullRow = (body, html, rowClass = '', cellClass = '') => `<tr${rowClass ? ` class="${rowClass}"` : ''}>${fullCell(body, html, cellClass)}</tr>`;
|
||||
// Names and values in two columns, for a detail row.
|
||||
const kvGrid = rows => `<div class="kv-grid mono">${rows.map(([k, v]) => `<div>${k}</div><div>${v}</div>`).join('')}</div>`;
|
||||
|
||||
// What a community of ours asks for where control communities are on
|
||||
// (control_communities), or ''. They are acted on where a route leaves
|
||||
@@ -498,8 +512,10 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
'route server': ['RS', 'route server of an Internet exchange'],
|
||||
'route server client': ['RSC', 'client of a route server we run'],
|
||||
};
|
||||
// A router of our own AS, by its AS number.
|
||||
const isIbgp = p => p.peer_asn === (state.status || {}).local_asn;
|
||||
function roleGlyph(p) {
|
||||
if (p.peer_asn === (state.status || {}).local_asn) return glyph('iBGP', 'a router of our own AS');
|
||||
if (isIbgp(p)) 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' : '';
|
||||
@@ -531,7 +547,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
}
|
||||
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';
|
||||
if (isIbgp(p)) 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' };
|
||||
@@ -542,7 +558,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
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="10" class="dim">No peers configured and none connected.</td></tr>'); return; }
|
||||
if (!peers.length) { setHTML(b, fullRow(b, 'No peers configured and none connected.', '', 'dim')); return; }
|
||||
const problemsOnly = $('peers-problems').checked;
|
||||
// The IPv4 and IPv6 sessions of one neighbour, named <stem>-v4 and
|
||||
// <stem>-v6, are one neighbour to the reader: listed together, IPv6
|
||||
@@ -585,16 +601,16 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
// they are to us.
|
||||
const problems = peers.filter(p => peerRank(p) <= 1).sort(byName);
|
||||
if (problems.length) {
|
||||
html += `<tr class="group"><td colspan="10"><span class="red">Needing attention</span> (${problems.length})</td></tr>` + rows(problems);
|
||||
html += fullRow(b, `<span class="red">Needing attention</span> (${problems.length})`, 'group') + rows(problems);
|
||||
}
|
||||
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="10">${GROUP_TITLE[group]} (${members.length})</td></tr>` + rows(members);
|
||||
html += fullRow(b, `${GROUP_TITLE[group]} (${members.length})`, 'group') + rows(members);
|
||||
}
|
||||
} else if (!problems.length) {
|
||||
html += '<tr><td colspan="10" class="dim">Nothing needs attention.</td></tr>';
|
||||
html += fullRow(b, 'Nothing needs attention.', '', 'dim');
|
||||
}
|
||||
setHTML(b, html);
|
||||
renderTcpRows();
|
||||
@@ -610,10 +626,6 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
const open = state.tcpOpen.has(p.peer_addr);
|
||||
return `<a href="#" class="tcp-link${open ? ' open' : ''}" data-peer="${esc(p.peer_addr)}" title="TCP_INFO for this session's socket">tcp</a>`;
|
||||
}
|
||||
const fmtOctets = n => n >= 1048576 ? `${(n / 1048576).toFixed(1)} MiB` : n >= 1024 ? `${(n / 1024).toFixed(1)} KiB` : `${n} B`;
|
||||
const fmtBits = n => { const b = n * 8; return b >= 1e9 ? `${(b / 1e9).toFixed(2)} Gbit/s` : b >= 1e6 ? `${(b / 1e6).toFixed(1)} Mbit/s` : `${(b / 1e3).toFixed(0)} kbit/s`; };
|
||||
const fmtMs = n => n >= 1000 ? `${(n / 1000).toFixed(1)} s` : `${n.toFixed(n < 10 ? 2 : 0)} ms`;
|
||||
const ago = ms => ms >= 60000 ? `${Math.round(ms / 60000)}m` : ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
|
||||
function tcpDetail(a) {
|
||||
if (!a.available) return `<span class="dim">${esc(a.reason || 'n/a')}</span>`;
|
||||
const c = a.connection;
|
||||
@@ -634,24 +646,25 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
['bytes', `${kv('sent', fmtOctets(c.bytes_sent))} ${kv('acked', fmtOctets(c.bytes_acked))} ${kv('received', fmtOctets(c.bytes_received))} ${kv('segs', `${fmtCount(c.segments_out)}↑ ${fmtCount(c.segments_in)}↓`)}`],
|
||||
['last', `${kv('snd', ago(c.last_data_sent_ms))} ${kv('rcv', ago(c.last_data_received_ms))} ${kv('ack', ago(c.last_ack_received_ms))}`],
|
||||
];
|
||||
return `<div class="tcp-grid mono">${rows.map(([k, v]) => `<div>${k}</div><div>${v}</div>`).join('')}</div>`;
|
||||
return kvGrid(rows);
|
||||
}
|
||||
async function renderTcpRows() {
|
||||
const b = $('peers-body');
|
||||
for (const addr of state.tcpOpen) {
|
||||
const tr = document.querySelector(`#peers-body tr[data-peer-addr="${CSS.escape(addr)}"]`);
|
||||
const tr = b.querySelector(`tr[data-peer-addr="${CSS.escape(addr)}"]`);
|
||||
if (!tr) continue;
|
||||
let row = tr.nextElementSibling;
|
||||
if (!row || !row.classList.contains('tcp-row')) {
|
||||
row = document.createElement('tr');
|
||||
row.className = 'tcp-row';
|
||||
row.innerHTML = '<td colspan="10"><span class="dim">asking the kernel…</span></td>';
|
||||
row.className = 'detail tcp-row';
|
||||
row.innerHTML = fullCell(b, '<span class="dim">asking the kernel…</span>');
|
||||
tr.after(row);
|
||||
}
|
||||
try {
|
||||
const answer = await getJSON(`/api/tcp?peer=${encodeURIComponent(addr)}`);
|
||||
row.innerHTML = `<td colspan="10">${tcpDetail(answer)}</td>`;
|
||||
row.innerHTML = fullCell(b, tcpDetail(answer));
|
||||
} catch (e) {
|
||||
row.innerHTML = `<td colspan="10"><span class="red">${esc(String(e))}</span></td>`;
|
||||
row.innerHTML = fullCell(b, `<span class="red">${esc(String(e))}</span>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -790,7 +803,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
|
||||
function renderLocal() {
|
||||
const b = $('local-body'); $('local-count').textContent = state.local.length;
|
||||
if (!state.local.length) { setHTML(b, '<tr><td colspan="7" class="dim">This router originates no routes.</td></tr>'); return; }
|
||||
if (!state.local.length) { setHTML(b, fullRow(b, 'This router originates no routes.', '', 'dim')); return; }
|
||||
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>
|
||||
@@ -826,7 +839,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
: `${shown}${st ? `, for ${fmtCount(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, fullRow(b, looking ? 'No route matches.' : 'No routes received from peers.', '', 'dim')); return;
|
||||
}
|
||||
setHTML(b, state.received.map(r => { const s = r.summary; return `<tr class="${r.best ? 'best' : ''}" data-route="${esc(routeKey(r))}">
|
||||
<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>
|
||||
@@ -863,18 +876,19 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
if (r.path_id) rows.push(['PATH_ID', `${r.path_id}`]);
|
||||
rows.push(['selection', `${r.best ? 'best' : `rank ${r.rank}`}${r.stale ? ', stale' : ''}`]);
|
||||
rows.push(['rpki', r.rpki ? esc(r.rpki) : DASH], ['aspa', r.aspa ? esc(r.aspa) : DASH]);
|
||||
return `<div class="tcp-grid mono">${rows.map(([k, v]) => `<div>${k}</div><div>${v}</div>`).join('')}</div>`;
|
||||
return kvGrid(rows);
|
||||
}
|
||||
function renderRouteDetails() {
|
||||
const b = $('received-body');
|
||||
for (const key of state.routeOpen) {
|
||||
const r = state.received.find(r => routeKey(r) === key);
|
||||
const tr = document.querySelector(`#received-body tr[data-route="${CSS.escape(key)}"]`);
|
||||
const tr = b.querySelector(`tr[data-route="${CSS.escape(key)}"]`);
|
||||
if (!r || !tr) continue;
|
||||
const next = tr.nextElementSibling;
|
||||
if (next && next.classList.contains('route-detail')) continue;
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'route-detail';
|
||||
row.innerHTML = `<td colspan="12">${routeDetail(r)}</td>`;
|
||||
row.className = 'detail route-detail';
|
||||
row.innerHTML = fullCell(b, routeDetail(r));
|
||||
tr.after(row);
|
||||
}
|
||||
}
|
||||
@@ -961,7 +975,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
const total = state.rejected.reduce((n, p) => n + p.total, 0);
|
||||
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'}${chosen ? `; showing ${chosen}` : ''}` : '0';
|
||||
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; }
|
||||
if (!peers.length) { setHTML(b, fullRow(b, chosen ? `Nothing ${esc(chosen)} announced has been rejected.` : 'Nothing a peer announced has been rejected.', '', 'dim')); return; }
|
||||
setHTML(b, peers.map(p => {
|
||||
// Alike: the same prefix, for the same reason, by the same path.
|
||||
const alike = new Map();
|
||||
@@ -974,7 +988,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
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>` +
|
||||
return fullRow(b, `<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}`, 'group') +
|
||||
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">${clamp(asPath(r.as_path))}</td><td>${esc(r.reason)}</td>
|
||||
|
||||
Reference in new issue
Block a user