Origin validation says who may originate a prefix and nothing about the
rest of the path: it does not catch a route leak, nor a hijack that keeps
the right origin at the end of a made-up path. ASPAs, in which a network
lists its providers, do.
bgp-rpki:
- aspa: the ASPAs and the upstream and downstream verification procedures
of draft-ietf-sidrops-aspa-verification-24, tested against the draft's
wording as written.
- rtr: protocol version 2 (draft-ietf-sidrops-8210bis-23) with the ASPA
PDU and the new error codes.
- client: asks for version 2 and falls back to whatever older version the
cache speaks; keeps ASPAs with the VRPs, applied whole or not at all;
publishes which customers' ASPAs changed; tells the cache why it is
being left (Error Report); does not ask one version's serial of another.
Server:
- A peer has a role (provider, customer, peer, route server, route server
client), which decides how its paths are verified. Without one they are
not.
- A route whose path is invalid is refused ("ASPA invalid") and held, like
one with an invalid origin; accept_aspa_invalid exempts a peer.
- When ASPAs change, routes in the table whose path has a changed customer
on it are verified again, and held routes are released when they pass
both checks.
- The revalidator finds a route's peer by index instead of searching a
list for every route.
The status page and API show each peer's role, each route's path validity,
the number of ASPAs and the RTR version in use; bgp_rpki_aspas is on
/metricz.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
726 lines
26 KiB
Rust
726 lines
26 KiB
Rust
//! JSON handlers and their view types. Everything here is read-only: the
|
|
//! daemon is configured through its configuration file.
|
|
|
|
use std::collections::{BTreeMap, HashMap};
|
|
use std::net::IpAddr;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
use axum::Json;
|
|
|
|
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_primitives::constants::IpVersion;
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
use bgp_rib::table::{PeerIndex, RibTable, Route};
|
|
use bgp_rib::{LocalRoute, RibStopped};
|
|
use bgp_rpki::{CacheState, Update};
|
|
use bgp_server::policy::PeerRole;
|
|
use bgp_server::rpki::PathCheck;
|
|
use bgp_server::session::{ConfiguredPeer, DialState, PeerId, SessionHandle, SessionRegistry};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::AppState;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Views
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn unix_secs(t: SystemTime) -> u64 {
|
|
t.duration_since(UNIX_EPOCH)
|
|
.map(|d| d.as_secs())
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct StatusView {
|
|
pub router_id: String,
|
|
pub local_asn: u32,
|
|
pub version: String,
|
|
pub hostname: String,
|
|
pub started_at_unix: u64,
|
|
pub uptime_secs: u64,
|
|
pub sessions: usize,
|
|
pub local_routes: usize,
|
|
/// Routes learned from peers, counting each peer's separately.
|
|
pub received_routes: usize,
|
|
/// Prefixes with at least one such route, and so with a best route.
|
|
pub received_prefixes: usize,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct PeerView {
|
|
/// Name from the configuration file; absent for unconfigured inbound peers.
|
|
pub name: Option<String>,
|
|
pub peer_addr: String,
|
|
pub peer_asn: u32,
|
|
/// `Established`, `Connecting`, `Backoff` or `Idle`.
|
|
pub state: String,
|
|
/// `inbound` or `outbound`: which side opened the live TCP connection.
|
|
pub direction: Option<String>,
|
|
pub local_addr: Option<String>,
|
|
pub established_at_unix: Option<u64>,
|
|
pub uptime_secs: Option<u64>,
|
|
pub four_octet_asn: Option<bool>,
|
|
/// The address families negotiated with the peer, e.g. `IPv4 unicast`.
|
|
pub address_families: Option<Vec<String>>,
|
|
/// The families for which the peer sends us several routes for a
|
|
/// prefix (ADD-PATH, RFC 7911), and those for which we send it path
|
|
/// identifiers.
|
|
pub add_path_receive: Option<Vec<String>>,
|
|
pub add_path_send: Option<Vec<String>>,
|
|
/// How many of the peer's routes are in the routing table.
|
|
pub routes: Option<u64>,
|
|
/// How many routes the peer announced on this session that its import
|
|
/// policy refused.
|
|
pub rejected_routes: Option<u64>,
|
|
/// How many of the peer's routes are held out of the table as invalid
|
|
/// by the RPKI's data, to be taken if that changes.
|
|
pub held_routes: Option<u64>,
|
|
/// What the peer is to us, if configured: `provider`, `customer`,
|
|
/// `peer`, `route server` or `route server client`.
|
|
pub role: Option<&'static str>,
|
|
/// Why the last connection attempt or session ended, while in Backoff.
|
|
pub last_error: Option<String>,
|
|
pub retry_at_unix: Option<u64>,
|
|
}
|
|
|
|
impl From<&SessionHandle> for PeerView {
|
|
fn from(s: &SessionHandle) -> Self {
|
|
Self {
|
|
name: s.name.clone(),
|
|
peer_addr: s.peer.addr.to_string(),
|
|
peer_asn: s.peer.asn,
|
|
state: "Established".into(),
|
|
direction: Some(s.direction.as_str().into()),
|
|
local_addr: Some(s.local_addr.to_string()),
|
|
established_at_unix: Some(unix_secs(s.established_at)),
|
|
uptime_secs: Some(s.established_at.elapsed().map(|d| d.as_secs()).unwrap_or(0)),
|
|
four_octet_asn: Some(s.four_octet_asn),
|
|
address_families: Some(s.families.iter().map(|f| f.to_string()).collect()),
|
|
add_path_receive: Some(s.add_path_receive.iter().map(|f| f.to_string()).collect()),
|
|
add_path_send: Some(s.add_path_send.iter().map(|f| f.to_string()).collect()),
|
|
routes: Some(s.counters.routes()),
|
|
rejected_routes: Some(s.counters.rejected()),
|
|
held_routes: Some(s.counters.held()),
|
|
role: s.role.map(PeerRole::as_str),
|
|
last_error: None,
|
|
retry_at_unix: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PeerView {
|
|
fn from_configured(peer: &ConfiguredPeer, state: &DialState) -> Self {
|
|
let (state_name, last_error, retry_at_unix) = match state {
|
|
DialState::Idle => ("Idle", None, None),
|
|
DialState::Connecting => ("Connecting", None, None),
|
|
DialState::Backoff { until, last_error } => {
|
|
("Backoff", Some(last_error.clone()), Some(unix_secs(*until)))
|
|
}
|
|
};
|
|
Self {
|
|
name: Some(peer.name.clone()),
|
|
peer_addr: peer.endpoint.to_string(),
|
|
peer_asn: peer.remote_asn,
|
|
state: state_name.into(),
|
|
direction: None,
|
|
local_addr: None,
|
|
established_at_unix: None,
|
|
uptime_secs: None,
|
|
four_octet_asn: None,
|
|
address_families: None,
|
|
add_path_receive: None,
|
|
add_path_send: None,
|
|
routes: None,
|
|
rejected_routes: None,
|
|
held_routes: None,
|
|
role: peer.role.map(PeerRole::as_str),
|
|
last_error,
|
|
retry_at_unix,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Configured peers first (with their live session when established), then
|
|
/// any unconfigured inbound sessions.
|
|
pub fn peer_views(registry: &SessionRegistry) -> Vec<PeerView> {
|
|
let sessions = registry.list();
|
|
let mut claimed = std::collections::HashSet::new();
|
|
let mut views: Vec<PeerView> = registry
|
|
.configured_peers()
|
|
.iter()
|
|
.map(|(peer, state)| {
|
|
match sessions
|
|
.iter()
|
|
.find(|s| s.peer.addr.ip() == peer.endpoint.ip() && s.peer.asn == peer.remote_asn)
|
|
{
|
|
Some(session) => {
|
|
claimed.insert(session.peer);
|
|
PeerView {
|
|
name: Some(peer.name.clone()),
|
|
..PeerView::from(session)
|
|
}
|
|
}
|
|
None => PeerView::from_configured(peer, state),
|
|
}
|
|
})
|
|
.collect();
|
|
let mut rest: Vec<PeerView> = sessions
|
|
.iter()
|
|
.filter(|s| !claimed.contains(&s.peer))
|
|
.map(PeerView::from)
|
|
.collect();
|
|
rest.sort_by(|a, b| a.peer_addr.cmp(&b.peer_addr));
|
|
views.extend(rest);
|
|
views
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct LocalRouteView {
|
|
pub prefix: String,
|
|
pub origin: String,
|
|
pub prepend: u8,
|
|
pub med: Option<u32>,
|
|
pub communities: Vec<String>,
|
|
pub large_communities: Vec<String>,
|
|
pub announced_at_unix: u64,
|
|
}
|
|
|
|
impl From<&LocalRoute> for LocalRouteView {
|
|
fn from(r: &LocalRoute) -> Self {
|
|
Self {
|
|
prefix: r.prefix.to_string(),
|
|
origin: origin_name(r.attrs.origin).to_string(),
|
|
prepend: r.attrs.prepend,
|
|
med: r.attrs.med,
|
|
communities: r
|
|
.attrs
|
|
.communities
|
|
.iter()
|
|
.copied()
|
|
.map(community::format_standard)
|
|
.collect(),
|
|
large_communities: r
|
|
.attrs
|
|
.large_communities
|
|
.iter()
|
|
.map(community::format_large)
|
|
.collect(),
|
|
announced_at_unix: unix_secs(r.announced_at),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Human-oriented digest of a path attribute list.
|
|
#[derive(Debug, Clone, Default, Serialize)]
|
|
pub struct AttrSummary {
|
|
pub origin: Option<String>,
|
|
/// AS_PATH flattened in order; AS_SETs are included in place.
|
|
pub as_path: Vec<u32>,
|
|
pub next_hop: Option<String>,
|
|
pub med: Option<u32>,
|
|
pub local_pref: Option<u32>,
|
|
pub communities: Vec<String>,
|
|
pub large_communities: Vec<String>,
|
|
}
|
|
|
|
pub fn summarize(attributes: &[PathAttribute], next_hop: Option<&NlriNextHop>) -> AttrSummary {
|
|
let mut s = AttrSummary {
|
|
// A multiprotocol route has its next hop here; an IPv4 route has it
|
|
// in the NEXT_HOP attribute below.
|
|
next_hop: next_hop.map(ToString::to_string),
|
|
..Default::default()
|
|
};
|
|
for attr in attributes {
|
|
match attr {
|
|
PathAttribute::Origin(o) => s.origin = Some(origin_name(*o).to_string()),
|
|
PathAttribute::AsPath(p) => {
|
|
s.as_path = p
|
|
.segments
|
|
.iter()
|
|
.flat_map(|seg| seg.path.iter().copied())
|
|
.collect()
|
|
}
|
|
PathAttribute::NextHop(nh) => s.next_hop = Some(nh.0.to_string()),
|
|
PathAttribute::MultiExitDisc(m) => s.med = Some(m.0),
|
|
PathAttribute::LocalPref(l) => s.local_pref = Some(l.0),
|
|
PathAttribute::Communities(c) => {
|
|
s.communities =
|
|
c.0.iter()
|
|
.copied()
|
|
.map(community::format_standard)
|
|
.collect()
|
|
}
|
|
PathAttribute::LargeCommunities(l) => {
|
|
s.large_communities = l.communities.iter().map(community::format_large).collect()
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
s
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct ReceivedRouteView {
|
|
/// The peer's configured name, if it has one.
|
|
pub peer_name: Option<String>,
|
|
pub peer_addr: String,
|
|
pub peer_asn: u32,
|
|
pub prefix: String,
|
|
/// The peer's identifier for the route: zero unless the peer sends
|
|
/// several routes for a prefix (ADD-PATH, RFC 7911).
|
|
pub path_id: u32,
|
|
/// Whether this is the route chosen for its prefix.
|
|
pub best: bool,
|
|
/// What route origin validation says of the route: `valid` or
|
|
/// `not found`. (An invalid route is not in the table, unless its
|
|
/// peer's are taken regardless; then `invalid`.) Absent where
|
|
/// validation is not configured.
|
|
pub rpki: Option<&'static str>,
|
|
/// What the ASPAs say of the route's path: `valid` or `unknown`
|
|
/// (`invalid` only if the peer's are taken regardless). Absent where
|
|
/// validation is not configured, for a peer whose role is not, and
|
|
/// for one in our own AS.
|
|
pub aspa: Option<&'static str>,
|
|
pub summary: AttrSummary,
|
|
/// The decoded attribute list. MP_REACH_NLRI is not in it: its next hop
|
|
/// is in the summary and its prefixes are routes of their own.
|
|
pub attributes: Vec<PathAttribute>,
|
|
}
|
|
|
|
/// Who the routes in the table are from: the table knows peers by number,
|
|
/// the sessions know which number is whose.
|
|
pub(crate) struct PeerNames {
|
|
peers: HashMap<PeerIndex, (PeerId, Option<String>, Option<PathCheck>)>,
|
|
/// What routes are validated against, where they are.
|
|
rpki: Option<Update>,
|
|
}
|
|
|
|
impl PeerNames {
|
|
pub(crate) fn of(state: &AppState) -> Self {
|
|
Self {
|
|
peers: state
|
|
.registry
|
|
.list()
|
|
.into_iter()
|
|
.map(|session| {
|
|
// How the peer's paths are verified, or would be if
|
|
// its invalid routes were not taken regardless.
|
|
let check = session
|
|
.role
|
|
.filter(|_| !session.ibgp)
|
|
.map(|role| PathCheck::new(role, session.peer.asn));
|
|
(session.rib_peer, (session.peer, session.name, check))
|
|
})
|
|
.collect(),
|
|
rpki: state.rpki.as_ref().map(|rpki| rpki.current()),
|
|
}
|
|
}
|
|
|
|
/// The peers that `wanted` names: by configured name, or by address.
|
|
fn matching(&self, wanted: &str) -> Vec<PeerIndex> {
|
|
let address: Option<IpAddr> = wanted.parse().ok();
|
|
self.peers
|
|
.iter()
|
|
.filter(|(_, (peer, name, _))| {
|
|
name.as_deref() == Some(wanted) || address == Some(peer.addr.ip())
|
|
})
|
|
.map(|(index, _)| *index)
|
|
.collect()
|
|
}
|
|
|
|
/// A view of one route. `None` if the route's session has just ended:
|
|
/// the route is on its way out of the table.
|
|
pub(crate) fn view(&self, route: &Route) -> Option<ReceivedRouteView> {
|
|
let (peer, name, check) = self.peers.get(&route.peer)?;
|
|
let origin = Some(route.attrs.key().origin_as).filter(|origin| *origin != 0);
|
|
// What a session stored was accepted by the decoder, and parses.
|
|
let (attributes, next_hop) = route.attrs.attributes().unwrap_or_default();
|
|
Some(ReceivedRouteView {
|
|
peer_name: name.clone(),
|
|
peer_addr: peer.addr.to_string(),
|
|
peer_asn: peer.asn,
|
|
prefix: route.prefix.to_string(),
|
|
path_id: route.path_id,
|
|
best: route.best,
|
|
rpki: self
|
|
.rpki
|
|
.as_ref()
|
|
.map(|rpki| rpki.vrps.validate(&route.prefix, origin).as_str()),
|
|
aspa: self.rpki.as_ref().zip(*check).map(|(rpki, check)| {
|
|
let mut path = Vec::new();
|
|
let has_set = route.attrs.as_path(&mut path);
|
|
check.verify(&rpki.aspas, &path, has_set).as_str()
|
|
}),
|
|
summary: summarize(&attributes, next_hop.as_ref()),
|
|
attributes,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn origin_name(o: OriginPathAttribute) -> &'static str {
|
|
match o {
|
|
OriginPathAttribute::IGP => "IGP",
|
|
OriginPathAttribute::EGP => "EGP",
|
|
OriginPathAttribute::INCOMPLETE => "INCOMPLETE",
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Handlers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// A table could not be read because its actor has stopped. The daemon is
|
|
/// on its way down; saying so beats answering with an empty table.
|
|
pub struct Unavailable(RibStopped);
|
|
|
|
impl From<RibStopped> for Unavailable {
|
|
fn from(e: RibStopped) -> Self {
|
|
Self(e)
|
|
}
|
|
}
|
|
|
|
impl IntoResponse for Unavailable {
|
|
fn into_response(self) -> Response {
|
|
(StatusCode::SERVICE_UNAVAILABLE, self.0.to_string()).into_response()
|
|
}
|
|
}
|
|
|
|
pub async fn status(State(state): State<AppState>) -> Result<Json<StatusView>, Unavailable> {
|
|
Ok(Json(StatusView {
|
|
router_id: state.info.router_id.to_string(),
|
|
local_asn: state.info.local_asn,
|
|
version: state.info.version.clone(),
|
|
hostname: state.info.hostname.clone(),
|
|
started_at_unix: unix_secs(state.info.started_at),
|
|
uptime_secs: state.info.started.elapsed().as_secs(),
|
|
sessions: state.registry.list().len(),
|
|
local_routes: state.local_rib.len().await?,
|
|
received_routes: state.rib.len(),
|
|
received_prefixes: state.rib.prefix_count(),
|
|
}))
|
|
}
|
|
|
|
/// The configuration file the daemon was started with.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct ConfigView {
|
|
pub path: Option<String>,
|
|
/// Raw file contents as read at startup; later edits to the file are not
|
|
/// reflected until the daemon restarts.
|
|
pub text: Option<String>,
|
|
}
|
|
|
|
pub async fn config(State(state): State<AppState>) -> Json<ConfigView> {
|
|
let (path, text) = state.info.config.clone().unzip();
|
|
Json(ConfigView { path, text })
|
|
}
|
|
|
|
pub async fn peers(State(state): State<AppState>) -> Json<Vec<PeerView>> {
|
|
Json(peer_views(&state.registry))
|
|
}
|
|
|
|
pub async fn local_routes(
|
|
State(state): State<AppState>,
|
|
) -> Result<Json<Vec<LocalRouteView>>, Unavailable> {
|
|
let mut routes: Vec<LocalRouteView> = state
|
|
.local_rib
|
|
.snapshot()
|
|
.await?
|
|
.iter()
|
|
.map(LocalRouteView::from)
|
|
.collect();
|
|
routes.sort_by(|a, b| a.prefix.cmp(&b.prefix));
|
|
Ok(Json(routes))
|
|
}
|
|
|
|
/// How many routes [`received_routes`] returns unless asked for another
|
|
/// number. The table can hold hundreds of millions.
|
|
const DEFAULT_ROUTE_LIMIT: usize = 1000;
|
|
|
|
/// The most routes [`received_routes`] returns, whatever is asked for:
|
|
/// each is copied out of the table and written as JSON.
|
|
const MAX_ROUTE_LIMIT: usize = 100_000;
|
|
|
|
/// Which routes to return. At most one of `prefix`, `address` and `within`
|
|
/// says where to look; `peer` and `best` narrow what is found there.
|
|
#[derive(Debug, Default, Deserialize)]
|
|
pub struct RouteQuery {
|
|
/// At most this many routes.
|
|
limit: Option<usize>,
|
|
/// The routes for exactly this prefix, e.g. `193.0.0.0/21`.
|
|
prefix: Option<String>,
|
|
/// The routes for every prefix that covers this address, the most
|
|
/// specific prefix first: what a packet to the address could follow.
|
|
address: Option<String>,
|
|
/// The routes for this prefix and every prefix inside it.
|
|
within: Option<String>,
|
|
/// Only routes from this peer: its configured name, or its address.
|
|
peer: Option<String>,
|
|
/// Only the best route of each prefix.
|
|
#[serde(default)]
|
|
best: bool,
|
|
}
|
|
|
|
/// Why a query was not answered.
|
|
pub enum QueryError {
|
|
/// The query makes no sense; the text says how.
|
|
Bad(String),
|
|
/// Answering it failed here.
|
|
Failed,
|
|
}
|
|
|
|
impl IntoResponse for QueryError {
|
|
fn into_response(self) -> Response {
|
|
match self {
|
|
Self::Bad(why) => (StatusCode::BAD_REQUEST, why).into_response(),
|
|
Self::Failed => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Where in the table a query looks.
|
|
enum Scope {
|
|
/// Everywhere.
|
|
All,
|
|
Exactly(IpPrefix),
|
|
Covering(IpPrefix),
|
|
Within(IpPrefix),
|
|
}
|
|
|
|
impl Scope {
|
|
fn of(query: &RouteQuery) -> Result<Self, QueryError> {
|
|
let prefix = |text: &str| {
|
|
IpPrefix::try_from(text)
|
|
.map_err(|e| QueryError::Bad(format!("{text:?} is not a prefix: {e}")))
|
|
};
|
|
match (&query.prefix, &query.address, &query.within) {
|
|
(None, None, None) => Ok(Self::All),
|
|
(Some(text), None, None) => Ok(Self::Exactly(prefix(text)?)),
|
|
(None, Some(text), None) => {
|
|
let address: IpAddr = text
|
|
.parse()
|
|
.map_err(|_| QueryError::Bad(format!("{text:?} is not an IP address")))?;
|
|
// The address as a prefix as long as an address is.
|
|
let host = match address {
|
|
IpAddr::V4(v4) => IpPrefix::new(IpVersion::Ipv4, v4.octets(), 32),
|
|
IpAddr::V6(v6) => IpPrefix::new(IpVersion::Ipv6, v6.octets(), 128),
|
|
};
|
|
host.map(Self::Covering)
|
|
.map_err(|e| QueryError::Bad(e.to_string()))
|
|
}
|
|
(None, None, Some(text)) => Ok(Self::Within(prefix(text)?)),
|
|
_ => Err(QueryError::Bad(
|
|
"give at most one of prefix, address and within".into(),
|
|
)),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The routes a query asks for, in the order to show them.
|
|
fn find_routes(
|
|
table: &RibTable,
|
|
names: &PeerNames,
|
|
scope: &Scope,
|
|
query: &RouteQuery,
|
|
limit: usize,
|
|
) -> Vec<ReceivedRouteView> {
|
|
let peers = query.peer.as_deref().map(|peer| names.matching(peer));
|
|
let wanted = |peer: PeerIndex, best: bool| {
|
|
(!query.best || best) && peers.as_ref().is_none_or(|peers| peers.contains(&peer))
|
|
};
|
|
let mut routes = match scope {
|
|
Scope::Exactly(prefix) => table.routes(prefix),
|
|
// From the longest prefix that could cover the address to the
|
|
// shortest: one lookup for each length.
|
|
Scope::Covering(host) => (0..=host.length())
|
|
.rev()
|
|
.flat_map(|length| table.routes(&host.shortened(length)))
|
|
.collect(),
|
|
Scope::Within(outer) => table.routes_where(limit, |route| {
|
|
outer.contains(&route.prefix) && wanted(route.peer, route.best)
|
|
}),
|
|
Scope::All => table.routes_where(limit, |route| wanted(route.peer, route.best)),
|
|
};
|
|
routes.retain(|route| wanted(route.peer, route.best));
|
|
routes.truncate(limit);
|
|
|
|
// For each prefix the best route first. The prefixes in address
|
|
// order; for a lookup by address, which finds one prefix of each
|
|
// length, the most specific first.
|
|
if matches!(scope, Scope::Covering(_)) {
|
|
routes.sort_by_key(|route| (std::cmp::Reverse(route.prefix.length()), !route.best));
|
|
} else {
|
|
routes.sort_by_key(|route| (sort_key(&route.prefix), !route.best));
|
|
}
|
|
routes
|
|
.iter()
|
|
.filter_map(|route| names.view(route))
|
|
.collect()
|
|
}
|
|
|
|
/// Orders prefixes by IP version, then address, then shorter before
|
|
/// longer.
|
|
fn sort_key(prefix: &IpPrefix) -> (u16, [u8; 16], u8) {
|
|
let mut address = [0u8; 16];
|
|
address[..prefix.bytes().len()].copy_from_slice(prefix.bytes());
|
|
(prefix.address_family() as u16, address, prefix.length())
|
|
}
|
|
|
|
/// Routes learned from peers. Without a query, all the routes of as many
|
|
/// prefixes as fit in the limit; see [`RouteQuery`] for looking up
|
|
/// particular ones. `/api/status` has the totals.
|
|
pub async fn received_routes(
|
|
State(state): State<AppState>,
|
|
Query(query): Query<RouteQuery>,
|
|
) -> Result<Json<Vec<ReceivedRouteView>>, QueryError> {
|
|
let limit = query
|
|
.limit
|
|
.unwrap_or(DEFAULT_ROUTE_LIMIT)
|
|
.min(MAX_ROUTE_LIMIT);
|
|
let scope = Scope::of(&query)?;
|
|
let names = PeerNames::of(&state);
|
|
// Looking through the table, copying, parsing and sorting many routes
|
|
// takes long enough to keep off the threads that sessions run on.
|
|
let routes =
|
|
tokio::task::spawn_blocking(move || find_routes(&state.rib, &names, &scope, &query, limit))
|
|
.await
|
|
// Only if making the views panicked.
|
|
.map_err(|_| QueryError::Failed)?;
|
|
Ok(Json(routes))
|
|
}
|
|
|
|
/// Validation with the RPKI: where the data comes from and how fresh it
|
|
/// is.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct RpkiView {
|
|
/// Whether validation is configured at all.
|
|
pub enabled: bool,
|
|
/// The cache in use, or last tried.
|
|
pub cache: Option<String>,
|
|
/// `connecting`, `synchronising` or `synchronised`.
|
|
pub state: Option<&'static str>,
|
|
pub session_id: Option<u16>,
|
|
pub serial: Option<u32>,
|
|
pub protocol_version: Option<u8>,
|
|
pub vrps_ipv4: usize,
|
|
pub vrps_ipv6: usize,
|
|
/// How many ASes have an ASPA. A cache that speaks a version of the
|
|
/// protocol before 2 has none to give.
|
|
pub aspas: usize,
|
|
pub updated_at_unix: Option<u64>,
|
|
/// When the data is dropped unless a cache confirms it first.
|
|
pub expires_at_unix: Option<u64>,
|
|
pub last_error: Option<String>,
|
|
}
|
|
|
|
pub async fn rpki(State(state): State<AppState>) -> Json<RpkiView> {
|
|
let Some(status) = state.rpki.as_ref().map(|rpki| rpki.status()) else {
|
|
return Json(RpkiView {
|
|
enabled: false,
|
|
cache: None,
|
|
state: None,
|
|
session_id: None,
|
|
serial: None,
|
|
protocol_version: None,
|
|
vrps_ipv4: 0,
|
|
vrps_ipv6: 0,
|
|
aspas: 0,
|
|
updated_at_unix: None,
|
|
expires_at_unix: None,
|
|
last_error: None,
|
|
});
|
|
};
|
|
Json(RpkiView {
|
|
enabled: true,
|
|
cache: status.cache.map(|cache| cache.to_string()),
|
|
state: Some(match status.state {
|
|
CacheState::Connecting => "connecting",
|
|
CacheState::Synchronising => "synchronising",
|
|
CacheState::Synchronised => "synchronised",
|
|
}),
|
|
session_id: status.session_id,
|
|
serial: status.serial,
|
|
protocol_version: Some(status.protocol_version),
|
|
vrps_ipv4: status.vrps_ipv4,
|
|
vrps_ipv6: status.vrps_ipv6,
|
|
aspas: status.aspas,
|
|
updated_at_unix: status.updated_at.map(unix_secs),
|
|
expires_at_unix: status.expires_at.map(unix_secs),
|
|
last_error: status.last_error,
|
|
})
|
|
}
|
|
|
|
/// What one peer announced that was refused.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct RefusalsView {
|
|
/// The peer's configured name, or its address if it has none.
|
|
pub peer: String,
|
|
pub peer_addr: String,
|
|
/// How many routes were refused since the daemon started.
|
|
pub total: u64,
|
|
/// The same, by reason.
|
|
pub by_reason: BTreeMap<&'static str, u64>,
|
|
/// The latest of them, newest first.
|
|
pub recent: Vec<RefusedRouteView>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct RefusedRouteView {
|
|
pub prefix: String,
|
|
pub path_id: u32,
|
|
pub reason: &'static str,
|
|
pub as_path: Vec<u32>,
|
|
pub at_unix: u64,
|
|
}
|
|
|
|
#[derive(Debug, Default, Deserialize)]
|
|
pub struct RefusalQuery {
|
|
/// Only this peer: its configured name, or its address.
|
|
peer: Option<String>,
|
|
}
|
|
|
|
/// The routes that peers announced and their import policies refused: for
|
|
/// each peer that had any, how many and why, and the latest of them.
|
|
pub async fn rejected_routes(
|
|
State(state): State<AppState>,
|
|
Query(query): Query<RefusalQuery>,
|
|
) -> Json<Vec<RefusalsView>> {
|
|
let mut peers: Vec<RefusalsView> = state
|
|
.refusals
|
|
.peers()
|
|
.into_iter()
|
|
.filter(|(address, record)| {
|
|
query
|
|
.peer
|
|
.as_deref()
|
|
.is_none_or(|wanted| record.label() == wanted || wanted.parse() == Ok(*address))
|
|
})
|
|
.map(|(address, record)| RefusalsView {
|
|
peer: record.label().to_string(),
|
|
peer_addr: address.to_string(),
|
|
total: record.total(),
|
|
by_reason: record
|
|
.by_reason()
|
|
.into_iter()
|
|
.map(|(reason, count)| (reason.as_str(), count))
|
|
.collect(),
|
|
recent: record
|
|
.recent()
|
|
.into_iter()
|
|
.map(|route| RefusedRouteView {
|
|
prefix: route.prefix.to_string(),
|
|
path_id: route.path_id,
|
|
reason: route.reason.as_str(),
|
|
as_path: route.as_path.to_vec(),
|
|
at_unix: unix_secs(route.at),
|
|
})
|
|
.collect(),
|
|
})
|
|
.collect();
|
|
peers.sort_by(|a, b| a.peer.cmp(&b.peer));
|
|
Json(peers)
|
|
}
|