//! HTTP surface of the daemon: a read-only JSON API for inspecting state, a //! status page, a server-sent event stream, and Prometheus metrics. //! //! Everything here is a thin layer over handles owned by the BGP server //! ([`SessionRegistry`], [`LocalRib`], [`RibTable`]); no routing state lives //! in this crate. mod api; mod events; mod metrics; use std::net::Ipv4Addr; use std::sync::Arc; use std::time::{Instant, SystemTime}; use axum::Router; use axum::response::{Html, Redirect}; use axum::routing::get; use bgp_fib::Fib; use bgp_rib::LocalRib; use bgp_rib::table::RibTable; use bgp_rpki::Rpki; use bgp_server::refusals::RefusalLog; use bgp_server::restart::Restarts; use bgp_server::session::SessionRegistry; use tokio::net::TcpListener; use tracing::info; pub use api::{ AttrSummary, ConfigView, KernelView, LocalRouteView, PeerView, ReceivedRouteView, RefusalsView, RefusedRouteView, RoutesPage, RpkiView, StatusView, }; /// Static facts about the router shown in status output. #[derive(Debug, Clone)] pub struct RouterInfo { pub router_id: Ipv4Addr, pub local_asn: u32, pub version: String, pub started: Instant, /// Wall-clock start time, for display. pub started_at: SystemTime, /// Name of the machine we run on, for display. pub hostname: String, /// The configuration file as read at startup (path and raw text), shown /// on the status page. `None` when the server was configured in code. pub config: Option<(String, String)>, /// Whether large communities of our AS ask for things here /// (`control_communities`), so that the status page can say what. pub control_communities: bool, } impl RouterInfo { pub fn new(router_id: Ipv4Addr, local_asn: u32, version: impl Into) -> Self { Self { router_id, local_asn, version: version.into(), started: Instant::now(), started_at: SystemTime::now(), hostname: hostname(), config: None, control_communities: false, } } /// Records whether control communities are acted on. pub fn with_control_communities(mut self, on: bool) -> Self { self.control_communities = on; self } /// Records the configuration file so the status page can show it. pub fn with_config(mut self, path: impl Into, text: impl Into) -> Self { self.config = Some((path.into(), text.into())); self } } /// Best-effort host name without pulling in a dependency: ask the `hostname` /// utility, which exists on every platform we run on. fn hostname() -> String { std::process::Command::new("hostname") .output() .ok() .filter(|o| o.status.success()) .and_then(|o| String::from_utf8(o.stdout).ok()) .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .unwrap_or_else(|| "unknown host".into()) } #[derive(Clone)] pub struct AppState { pub info: Arc, pub registry: SessionRegistry, pub local_rib: LocalRib, /// The routing table: routes learned from peers and the best of them. pub rib: Arc, /// What peers announced that was not taken. pub refusals: RefusalLog, /// The data for route origin validation, where that is configured. pub rpki: Option, /// What keeps the best routes in the kernel, where that is configured. pub kernel: Option, /// Graceful restart: the routes kept of restarting peers. pub restarts: Arc, } /// The status page, embedded so the binary is self-contained. const STATUSZ_HTML: &str = include_str!("../static/statusz.html"); async fn statusz() -> Html<&'static str> { Html(STATUSZ_HTML) } /// Builds the router. Exposed so tests can drive it without a socket. pub fn router(state: AppState) -> Router { Router::new() .route("/", get(|| async { Redirect::temporary("/statusz") })) .route("/statusz", get(statusz)) .route("/api/status", get(api::status)) .route("/api/config", get(api::config)) .route("/api/peers", get(api::peers)) .route("/api/routes/local", get(api::local_routes)) .route("/api/routes/received", get(api::received_routes)) .route("/api/routes/rejected", get(api::rejected_routes)) .route("/api/rpki", get(api::rpki)) .route("/api/kernel", get(api::kernel)) .route("/api/events", get(events::stream)) .route("/metricz", get(metrics::metricz)) .with_state(state) } /// Serves the API on `listener` until the task is dropped. The caller /// binds it, and so finds out before anything else starts if it cannot. pub async fn serve_on(state: AppState, listener: TcpListener) -> std::io::Result<()> { info!(addr = %listener.local_addr()?, "HTTP API listening"); axum::serve(listener, router(state)).await?; Ok(()) }