//! JSON handlers and their view types. Everything here is read-only: the //! daemon is configured through its configuration file. use std::time::{SystemTime, UNIX_EPOCH}; use axum::Json; use std::collections::HashMap; 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_rib::table::{PeerIndex, Route}; use bgp_rib::{LocalRoute, RibStopped}; 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>, /// 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()), 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, 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 { 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, 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(HashMap); impl PeerNames { pub(crate) fn of(registry: &SessionRegistry) -> Self { Self( registry .list() .into_iter() .map(|session| (session.rib_peer, session.peer)) .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 = self.0.get(&route.peer)?; // What a session stored was accepted by the decoder, and parses. let (attributes, next_hop) = route.attrs.attributes().unwrap_or_default(); Some(ReceivedRouteView { peer_addr: peer.addr.to_string(), peer_asn: peer.asn, prefix: route.prefix.to_string(), path_id: route.path_id, best: route.best, 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; #[derive(Debug, Deserialize)] pub struct RouteQuery { /// At most this many routes. limit: Option, } /// Some of the routes learned from peers: all the routes of as many /// prefixes as fit in the limit. `/api/status` has the totals. pub async fn received_routes( State(state): State, Query(query): Query, ) -> Result>, StatusCode> { let limit = query .limit .unwrap_or(DEFAULT_ROUTE_LIMIT) .min(MAX_ROUTE_LIMIT); let names = PeerNames::of(&state.registry); // 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 || { let mut routes: Vec = state .rib .some_routes(limit) .iter() .filter_map(|route| names.view(route)) .collect(); routes.sort_by(|a, b| (&a.prefix, &a.peer_addr).cmp(&(&b.prefix, &b.peer_addr))); routes }) .await // Only if making the views panicked. .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(routes)) }