status: a session's TCP connection as the kernel sees it; a route in full

When a session advertised a full table to a route collector at two
thousand routes a second, nothing on the status page said whether the
daemon or the peer set that pace. The answer is in the kernel: the
peer's receive window, the queues, and how long the connection was held
up by the window or by the send buffer, which `ss -ti` shows.

This change asks the kernel the same way `ss` does, over the socket
diagnostics netlink family (`NETLINK_SOCK_DIAG`, `SOCK_DIAG_BY_FAMILY`
with the connection's addresses and ports), and reads `tcp_info` and
the congestion control name from the answer. No external program is
run and no unsafe code is needed: the request and the answer are byte
layouts of the kernel's ABI, read field by field, with fields an older
kernel does not send left at zero. A socket on a link-local address is
found by its interface, which the request names.

* `bgp_fib::sockdiag`: the request, the parser, `TcpConnection`, and
  `tcp_connection(local, peer)`; `Ok(None)` where there is no such
  connection or the system is not Linux.
* `GET /api/tcp?peer=ADDRESS` in the HTTP API: the connection of the
  session with that peer, with times in milliseconds, the two windows
  named by what they allow, and the held-up times also as shares of the
  busy time.
* The status page: a "tcp" link on each established session, which
  expands a row below it with the figures, re-asked on every refresh
  while open. The group rows' span is corrected for the "sent" column.

Also on the status page: the attributes of a received route that have
no column of their own were badges with their value in a tooltip, which
is hard to read. Clicking the attrs cell now expands a row under the
route with every attribute by its name (ORIGIN, AS_PATH, NEXT_HOP, MED,
LOCAL_PREF, COMMUNITIES, LARGE_COMMUNITIES, AGGREGATOR and the rest),
the path identifier, the selection rank and the RPKI and ASPA states;
the badges stay. Both expansions use the terms of `ss` and the RFCs
rather than sentences.

Documentation: the endpoint in the HTTP API reference, and the link and
the expansion in the monitoring guide.

Testing: unit tests of the request and of the parser on a reply built
field by field, and of a "no such socket" and a refused answer; a
Linux-only test that asks the kernel about a loopback connection of the
test's own and about one that does not exist; the lab's basics phase
checks the endpoint for the FRR session; the page was checked by hand
against the daemon in the Linux VM.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-09 11:41:47 +02:00
1 parent efde277fd8
commit 99a47da2af
8 files changed
+807 -10

No files matched your search

+139 -9
View File
@@ -68,6 +68,14 @@ 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; }
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; }
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); }
table.data tr.dim td { color: var(--dim); }
.asn { cursor: help; }
@@ -241,7 +249,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
const $ = id => document.getElementById(id);
const esc = s => String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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, tcpOpen: new Set(), routeOpen: new Set() };
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.
@@ -532,7 +540,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="9" class="dim">No peers configured and none connected.</td></tr>'); return; }
if (!peers.length) { setHTML(b, '<tr><td colspan="10" class="dim">No peers configured and none connected.</td></tr>'); 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
@@ -549,7 +557,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
const expected = up && p.expected_routes > 0
? ` <span class="${short ? 'amber' : 'dim'}" title="${short ? 'Well under' : 'About'} the ${fmtCount(p.expected_routes)} routes this peer is expected to send (expected_routes)">of ~${fmtCount(p.expected_routes)}</span>` : '';
const classes = [peerRank(p) === 3 ? 'dim' : '', pair || ''].filter(Boolean).join(' ');
return `<tr class="${classes}" data-peer-row="${esc(p.name || p.peer_addr)}">
return `<tr class="${classes}" data-peer-row="${esc(p.name || p.peer_addr)}" data-peer-addr="${esc(p.peer_addr)}">
<td class="name">${sinceGlyph(p)}${name}</td>
<td class="mono">${esc(p.peer_addr)} <span class="dim">AS${asn(p.peer_asn)}</span> ${roleGlyph(p)}</td>
<td><span class="${STATE_CLASS[p.state] || ''}">${esc(p.state)}</span></td>
@@ -559,7 +567,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
<td class="num">${up ? trendGlyph(p) + fmtCount(p.routes) + expected + stale : DASH}</td>
<td class="num">${up ? fmtCount(p.advertised_routes) : DASH}</td>
<td class="num">${rejected}${held}</td>
<td>${peerDetail(p)}</td></tr>`;
<td>${peerDetail(p)}${up ? tcpLink(p) : ''}</td></tr>`;
};
// Rows for a sorted list, with the v4/v6 pairs in it marked.
const rows = list => list.map((p, i) => {
@@ -575,23 +583,93 @@ 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="9"><span class="red">Needing attention</span> (${problems.length})</td></tr>` + rows(problems);
html += `<tr class="group"><td colspan="10"><span class="red">Needing attention</span> (${problems.length})</td></tr>` + 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="9">${GROUP_TITLE[group]} (${members.length})</td></tr>` + rows(members);
html += `<tr class="group"><td colspan="10">${GROUP_TITLE[group]} (${members.length})</td></tr>` + rows(members);
}
} else if (!problems.length) {
html += '<tr><td colspan="9" class="dim">Nothing needs attention.</td></tr>';
html += '<tr><td colspan="10" class="dim">Nothing needs attention.</td></tr>';
}
setHTML(b, html);
renderTcpRows();
const marked = peers.filter(changedSince).length;
setHTML($('since-ctl'), marked ? `${glyph('●', 'Rows that changed while this tab was in the background', 'since')} <span class="dim">${marked} changed since you last looked ·</span> <a href="#" id="since-clear">clear</a>` : '');
renderPeerChoices();
}
// The TCP connection of a session, as the kernel sees it: a link on the
// row, and a row below it while it is open, asked for afresh on every
// refresh.
function tcpLink(p) {
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;
const pct = share => `${(share * 100).toFixed(0)}%`;
const held = share => share > 0.5 ? 'red' : share > 0.1 ? 'amber' : '';
const kv = (k, v) => `<span class="dim">${k}</span> ${v}`;
const rows = [
['state', `${esc(c.state.toUpperCase())} ${c.congestion ? esc(c.congestion) : ''}`],
['rtt', `${fmtMs(c.rtt_ms)} ${kv('minrtt', fmtMs(c.min_rtt_ms))} ${kv('rttvar', fmtMs(c.rtt_variance_ms))} ${kv('rto', fmtMs(c.rto_ms))}`],
['cwnd', `${fmtCount(c.cwnd_segments)} ${kv('mss', `${c.mss}`)} ${kv('=', fmtOctets(c.cwnd_segments * c.mss))} ${kv('ssthresh', c.ssthresh_segments == null ? '∞' : fmtCount(c.ssthresh_segments))} ${kv('pmtu', `${c.pmtu}`)}`],
['snd_wnd', `${fmtOctets(c.send_window)} ${kv('wscale', `${c.send_window_scale}`)}`],
['rcv_wnd', `${fmtOctets(c.receive_window)} ${kv('rcv_space', fmtOctets(c.receive_space))} ${kv('wscale', `${c.receive_window_scale}`)}`],
['send-q', `${fmtOctets(c.send_queue)} ${kv('notsent', fmtOctets(c.not_sent))} ${kv('unacked', fmtCount(c.unacked_segments))}`],
['recv-q', fmtOctets(c.recv_queue)],
['retrans', `${fmtCount(c.retrans_segments)} ${kv('lost', fmtCount(c.lost_segments))} ${kv('total', `${fmtCount(c.total_retrans)} / ${fmtOctets(c.bytes_retrans)}`)}`],
['rate', `${kv('delivery', fmtBits(c.delivery_rate))} ${kv('pacing', fmtBits(c.pacing_rate))}`],
['limited', `${kv('busy', fmtMs(c.busy_ms))} <span class="${held(c.limited_by_receive_window_share)}">${kv('rwnd', pct(c.limited_by_receive_window_share))}</span> <span class="${held(c.limited_by_send_buffer_share)}">${kv('sndbuf', pct(c.limited_by_send_buffer_share))}</span>`],
['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>`;
}
async function renderTcpRows() {
for (const addr of state.tcpOpen) {
const tr = document.querySelector(`#peers-body 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>';
tr.after(row);
}
try {
const answer = await getJSON(`/api/tcp?peer=${encodeURIComponent(addr)}`);
row.innerHTML = `<td colspan="10">${tcpDetail(answer)}</td>`;
} catch (e) {
row.innerHTML = `<td colspan="10"><span class="red">${esc(String(e))}</span></td>`;
}
}
}
document.addEventListener('click', e => {
const link = e.target.closest('a.tcp-link');
if (!link) return;
e.preventDefault();
const addr = link.dataset.peer;
if (state.tcpOpen.has(addr)) {
state.tcpOpen.delete(addr);
link.classList.remove('open');
const tr = link.closest('tr'), next = tr && tr.nextElementSibling;
if (next && next.classList.contains('tcp-row')) next.remove();
} else {
state.tcpOpen.add(addr);
link.classList.add('open');
renderTcpRows();
}
});
// The strip: what needs attention, in one line, or that nothing does.
function renderStrip() {
const items = [];
@@ -748,18 +826,70 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
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, state.received.map(r => { const s = r.summary; return `<tr class="${r.best ? 'best' : ''}">
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>
<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">${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>${clamp(commList(s.communities))}</td><td>${clamp(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 class="attrs" title="all attributes">${otherList(s.other)}</td>
<td class="nowrap">${validity('RPKI', r.rpki)} ${validity('ASPA', r.aspa)}</td></tr>`; }).join(''));
markTall(b);
renderRouteDetails();
}
// A route, in full, under its row: every attribute by its name, as the
// RFCs write it, opened by a click on the attrs cell.
const routeKey = r => `${r.prefix}|${r.peer_addr}|${r.path_id}`;
const ATTR_NAMES = {
'atomic aggregate': 'ATOMIC_AGGREGATE', 'aggregator': 'AGGREGATOR', 'extended communities': 'EXTENDED_COMMUNITIES',
'only to customer': 'OTC', 'originator ID': 'ORIGINATOR_ID', 'cluster list': 'CLUSTER_LIST',
};
function routeDetail(r) {
const s = r.summary;
const rows = [
['ORIGIN', s.origin ? esc(s.origin) : DASH],
['AS_PATH', s.as_path.length ? s.as_path.map(asn).join(' ') : '<span class="dim">(empty)</span>'],
['NEXT_HOP', s.next_hop ? esc(s.next_hop) : DASH],
['MED', s.med ?? DASH],
['LOCAL_PREF', s.local_pref ?? DASH],
['COMMUNITIES', s.communities.length ? s.communities.map(esc).join(' ') : DASH],
['LARGE_COMMUNITIES', s.large_communities.length ? s.large_communities.map(esc).join(' ') : DASH],
...(s.other || []).map(a => [esc(ATTR_NAMES[a.name] || a.name), esc(a.value) || 'present']),
];
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>`;
}
function renderRouteDetails() {
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)}"]`);
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>`;
tr.after(row);
}
}
document.addEventListener('click', e => {
const cell = e.target.closest('#received-body td.attrs');
if (!cell || e.target.closest('a')) return;
const tr = cell.closest('tr'), key = tr.dataset.route;
if (state.routeOpen.has(key)) {
state.routeOpen.delete(key);
const next = tr.nextElementSibling;
if (next && next.classList.contains('route-detail')) next.remove();
} else {
state.routeOpen.add(key);
renderRouteDetails();
}
});
const RPKI_STATE_CLASS = { synchronised: 'green', synchronising: 'amber', connecting: 'red' };
function renderRpki() {
const r = state.rpki, b = $('rpki-body');