Files
ferrix/crates/web/src/lib.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 db8edaae8e
ci / interop (push) Canceled after 0s
ci / check (push) Canceled after 0s
ci / docs (push) Canceled after 0s
ci / fuzz (push) Canceled after 0s
policy, config, web: the TCP gauges named once, one revalidator, one peer-naming rule
A code-quality pass over policy, configuration and the status API.
The twenty TCP gauges were spelled out in the struct, the
constructor, the removal, the sampler in the web crate and the status
view; one macro names each once with its help text, `PeerTcp::set`
fills them from a connection, and the view is a `From`. Every metric
and the list that forces them at start come from one declaration,
with a test that every declared family is registered. The two
revalidators, for next hops and for RPKI, share the task body that
scans the table off the sessions' threads and fans the findings out
to every session; the RPKI scan judges a route with the same `Checks`
the import path uses. Which peer a query names is one rule for the
received, rejected and TCP endpoints, so `/api/tcp` takes a name or an
address like the others. The config parser says proto3's "zero is
unset" in three helpers instead of a dozen shapes, and `parse_peer`
is three named parts shorter. The policy's test-only next-hop flag is
named for what it does, `accept_own_host_next_hop`; the "listed, or
else ours" prefix rule and the mark-then-prefer step each exist once.
The status page derives its spanning rows from the table's header,
shares one key-value grid and one detail-row rule, and defines its
helpers before their use. The web crate no longer re-exports views
nothing uses, and the TCP endpoint sits above the test module.

Testing: the TCP view's arithmetic (a share of no busy time is zero,
not NaN), the peer-naming rule, the RPKI scan finding only what a
change can have made invalid, and `/api/tcp` by address, by name and
for an unknown peer. The server and web tests, the HTTP, roles, RPKI
and import-policy integration tests, the docs build and every lab
phase pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 22:44:32 +02:00

156 lines
5.3 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 memory;
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::rejections::RejectionLog;
use bgp_server::restart::Restarts;
use bgp_server::session::SessionRegistry;
use serde::Serialize;
use tokio::net::TcpListener;
use tracing::info;
/// 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,
/// Names for communities, as configured (`community_names`), for the
/// status page to show in place of the numbers.
pub community_names: Vec<CommunityName>,
}
/// A name for a community, or for a kind of them.
#[derive(Debug, Clone, Serialize)]
pub struct CommunityName {
/// The community, or a pattern of them with `*` for a part left open:
/// `64601:100:2`, `64601:102:*`.
pub community: String,
pub name: String,
}
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,
community_names: Vec::new(),
}
}
/// Records the names configured for communities.
pub fn with_community_names(mut self, names: Vec<CommunityName>) -> Self {
self.community_names = names;
self
}
/// 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
}
}
/// The machine's host name as the kernel has it, for display; a machine
/// without one, or with one that is not text, is "unknown host".
fn hostname() -> String {
nix::unistd::gethostname()
.ok()
.and_then(|name| name.into_string().ok())
.filter(|name| !name.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 rejections: RejectionLog,
/// 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/tcp", get(api::tcp))
.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(())
}