//! 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, 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, pub local_addr: Option, pub established_at_unix: Option, pub uptime_secs: Option, pub four_octet_asn: Option, /// The address families negotiated with the peer, e.g. `IPv4 unicast`. pub address_families: Option>, /// 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>, pub add_path_send: Option>, /// How many of the peer's routes are in the routing table. pub routes: Option, /// How many routes the peer announced on this session that its import /// policy refused. pub rejected_routes: Option, /// 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, /// 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, pub retry_at_unix: Option, } 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 { let sessions = registry.list(); let mut claimed = std::collections::HashSet::new(); let mut views: Vec = 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 = 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, pub communities: Vec, pub large_communities: Vec, 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, /// AS_PATH flattened in order; AS_SETs are included in place. pub as_path: Vec, pub next_hop: Option, pub med: Option, pub local_pref: Option, pub communities: Vec, pub large_communities: Vec, } 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, 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, } /// 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, Option)>, /// What routes are validated against, where they are. rpki: Option, } 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 { let address: Option = 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 { 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 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) -> Result, 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, /// Raw file contents as read at startup; later edits to the file are not /// reflected until the daemon restarts. pub text: Option, } pub async fn config(State(state): State) -> Json { let (path, text) = state.info.config.clone().unzip(); Json(ConfigView { path, text }) } pub async fn peers(State(state): State) -> Json> { Json(peer_views(&state.registry)) } pub async fn local_routes( State(state): State, ) -> Result>, Unavailable> { let mut routes: Vec = 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, /// The routes for exactly this prefix, e.g. `193.0.0.0/21`. prefix: Option, /// The routes for every prefix that covers this address, the most /// specific prefix first: what a packet to the address could follow. address: Option, /// The routes for this prefix and every prefix inside it. within: Option, /// Only routes from this peer: its configured name, or its address. peer: Option, /// 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 { 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 { 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, Query(query): Query, ) -> Result>, 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, /// `connecting`, `synchronising` or `synchronised`. pub state: Option<&'static str>, pub session_id: Option, pub serial: Option, pub protocol_version: Option, 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, /// When the data is dropped unless a cache confirms it first. pub expires_at_unix: Option, pub last_error: Option, } pub async fn rpki(State(state): State) -> Json { 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, } #[derive(Debug, Clone, Serialize)] pub struct RefusedRouteView { pub prefix: String, pub path_id: u32, pub reason: &'static str, pub as_path: Vec, pub at_unix: u64, } #[derive(Debug, Default, Deserialize)] pub struct RefusalQuery { /// Only this peer: its configured name, or its address. peer: Option, } /// 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, Query(query): Query, ) -> Json> { let mut peers: Vec = 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) }