The library crates returned eyre reports, so a failure was a string and the session code could not tell a truncated UPDATE from a bad header. Whatever went wrong, the peer got an FSM error or nothing. Each crate now has its own error types: - bgp-primitives: PrefixError, UnknownAfi and UnknownSafi, and EncodeError for every to_wire. - bgp-packet: Message::decode returns a DecodeError naming the NOTIFICATION code, subcode and data for the failure (header errors per section 6.1, OPEN per 6.2, UPDATE per 6.3). InvalidValue and CommunityError replace the remaining strings. from_wire is now a thin wrapper around decode. - bgp-server: SessionError, ServerError and ConfigError. Every abnormal end of a session goes through one function that sends the NOTIFICATION the error maps to. The message length is checked against the 4096-octet maximum before the body is read. - bgp-web: serve returns io::Error. eyre remains in the binary and in tests. Bugs found and fixed on the way: - An OPEN with no optional parameters, which is valid, failed to parse. - An unsupported BGP version was reported as an unknown optional parameter. It now gets Unsupported Version Number with the version we speak as data. - A SAFI above 255 was truncated to its low octet and could match a known one. - Prefix text with a length beyond the address size (10.0.0.0/99) was accepted. - Message::to_wire truncated the body length to 16 bits before checking it. - A peer hold time above 65535 seconds in the configuration wrapped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
126 lines
4.1 KiB
Rust
126 lines
4.1 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`], [`AdjRibIn`]); no routing state lives
|
|
//! in this crate.
|
|
|
|
mod api;
|
|
mod events;
|
|
mod metrics;
|
|
|
|
use std::net::{Ipv4Addr, SocketAddr};
|
|
use std::sync::Arc;
|
|
use std::time::{Instant, SystemTime};
|
|
|
|
use axum::Router;
|
|
use axum::http::header;
|
|
use axum::response::{Html, IntoResponse, Redirect};
|
|
use axum::routing::get;
|
|
use bgp_rib::{AdjRibIn, LocalRib};
|
|
use bgp_server::session::SessionRegistry;
|
|
use tokio::net::TcpListener;
|
|
use tracing::info;
|
|
|
|
pub use api::{AttrSummary, ConfigView, LocalRouteView, PeerView, ReceivedRouteView, 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)>,
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
pub adj_rib_in: AdjRibIn,
|
|
}
|
|
|
|
/// The status page, embedded so the binary is self-contained.
|
|
const STATUSZ_HTML: &str = include_str!("../static/statusz.html");
|
|
|
|
async fn statusz() -> impl IntoResponse {
|
|
(
|
|
[(header::CONTENT_TYPE, "text/html; charset=utf-8")],
|
|
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/events", get(events::stream))
|
|
.route("/metricz", get(metrics::metricz))
|
|
.with_state(state)
|
|
}
|
|
|
|
/// Binds `listen` and serves the API until the task is dropped.
|
|
pub async fn serve(state: AppState, listen: SocketAddr) -> std::io::Result<()> {
|
|
let listener = TcpListener::bind(listen).await.map_err(|e| {
|
|
std::io::Error::new(
|
|
e.kind(),
|
|
format!("failed to bind HTTP listener on {listen}: {e}"),
|
|
)
|
|
})?;
|
|
serve_on(state, listener).await
|
|
}
|
|
|
|
/// Serves the API on an already-bound listener (lets tests use port 0).
|
|
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(())
|
|
}
|