Files
ferrix/crates/web/src/lib.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 175a13d506 Hygiene: scripts, CI, docs and manifests
- scripts/in-namespace.sh set CARGO_TARGET_DIR to the empty string when
  it was unset, which cargo refuses, and dropped RUSTFLAGS, so the run
  in the namespace rebuilt what kernel-tests.sh had just built. Each is
  passed on only where set.
- The README's list of unsupported features still had route reflection
  and graceful restart, both of which exist; the command-line reference
  lacked SIGUSR1 and exit status 75; the HTTP API reference did not say
  what errors look like.
- The event stream said "missed 0 events" for the best-routes stream,
  which does not count what it missed; it leaves the count out now.
- CI lints bgp-bench with its count-memory feature, which nothing built.
- bgp-bench had two unused dev-dependencies; bgp-integration-tests had
  three dependencies that only its tests and examples use.
- install.sh updates apt before installing; two-routers.sh loses an
  unused variable; small things in the web crate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 11:15:58 +02:00

142 lines
4.8 KiB
Rust

//! 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<String>) -> 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<String>, text: impl Into<String>) -> 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<RouterInfo>,
pub registry: SessionRegistry,
pub local_rib: LocalRib,
/// The routing table: routes learned from peers and the best of them.
pub rib: Arc<RibTable>,
/// What peers announced that was not taken.
pub refusals: RefusalLog,
/// The data for route origin validation, where that is configured.
pub rpki: Option<Rpki>,
/// What keeps the best routes in the kernel, where that is configured.
pub kernel: Option<Fib>,
/// Graceful restart: the routes kept of restarting peers.
pub restarts: Arc<Restarts>,
}
/// 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(())
}