Keep the path attributes of rejected routes, and show them
A rejected route was recorded with its prefix, reason and AS path, which is not enough to tell why a neighbor sent it: whether a /32 is a blackhole request, which communities a leaked route carries, which network marked it Only to Customer. Each recorded rejection now keeps the attributes of the UPDATE it came in, without the NLRI of MP_REACH_NLRI and MP_UNREACH_NLRI, and its next hop. The attributes are copied once per UPDATE, only when a route of it is rejected, and shared by its rejected routes. A route rejected later, when the RPKI data changes, has the attributes it was stored with. /api/routes/rejected gives them as `summary` and `attributes`, as for received routes, and the status page shows them under "show" in each row of the refused routes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e1d819d629
commit
bd674002a2
7 files changed
+166
-29
No files matched your search
@@ -950,6 +950,12 @@ pub struct RefusedRouteView {
|
||||
pub reason: &'static str,
|
||||
pub as_path: Vec<u32>,
|
||||
pub at_unix: u64,
|
||||
/// What the route came with, as for a received route: its next hop,
|
||||
/// origin, MED, local preference and communities.
|
||||
pub summary: AttrSummary,
|
||||
/// The decoded attribute list, as for a received route. MP_REACH_NLRI
|
||||
/// is not in it: its next hop is in the summary.
|
||||
pub attributes: Vec<PathAttribute>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
@@ -992,6 +998,8 @@ pub async fn rejected_routes(
|
||||
reason: route.reason.as_str(),
|
||||
as_path: route.as_path.to_vec(),
|
||||
at_unix: unix_secs(route.at),
|
||||
summary: summarize(&route.attributes, route.next_hop.as_ref()),
|
||||
attributes: route.attributes.to_vec(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
|
||||
@@ -27,6 +27,10 @@ table.data th { background: #dde; font-weight: bold; }
|
||||
#event-log { font-family: monospace; font-size: 12px; max-height: 22em; overflow-y: auto; }
|
||||
#event-log div { white-space: pre-wrap; }
|
||||
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; }
|
||||
@@ -100,7 +104,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
<h1 id="refused">Refused routes (<span id="refused-count">...</span>)</h1>
|
||||
<div class="dim">Routes that peers announced and their import policies did not take. The counts are since the daemon started; the latest refusals 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></tr></thead>
|
||||
<thead><tr><th>peer</th><th>prefix</th><th>AS path</th><th>reason</th><th>when</th><th>attributes</th></tr></thead>
|
||||
<tbody id="refused-body"></tbody>
|
||||
</table>
|
||||
|
||||
@@ -325,21 +329,37 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
||||
(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>`;
|
||||
}
|
||||
|
||||
// For each peer a line that says how many and why, then its latest few.
|
||||
const REFUSED_SHOWN = 20;
|
||||
function renderRefused() {
|
||||
const b = $('refused-body');
|
||||
const total = state.refused.reduce((n, p) => n + p.total, 0);
|
||||
$('refused-count').textContent = total ? `${total} from ${state.refused.length} peer${state.refused.length === 1 ? '' : 's'}` : '0';
|
||||
if (!state.refused.length) { setHTML(b, '<tr><td colspan="5" class="dim">Nothing a peer announced has been refused.</td></tr>'); return; }
|
||||
if (!state.refused.length) { setHTML(b, '<tr><td colspan="6" class="dim">Nothing a peer announced has been refused.</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="5"><b>${esc(p.peer)}</b> <span class="dim">${esc(p.peer_addr)}</span>: <span class="bad">${p.total} refused</span> (${why}).${more}</td></tr>` +
|
||||
return `<tr><td colspan="6"><b>${esc(p.peer)}</b> <span class="dim">${esc(p.peer_addr)}</span>: <span class="bad">${p.total} refused</span> (${why}).${more}</td></tr>` +
|
||||
p.recent.slice(0, REFUSED_SHOWN).map(r => `<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></tr>`).join('');
|
||||
<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('');
|
||||
}).join(''));
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user