Rework the speaker: sans-I/O FSM, sharded table, RFC 7606, ADD-PATH
The work of the speaker-cleanup branch as one commit; the branch keeps the step-by-step history. Sessions - The state machine is a pure function from events to actions, driven by one task per connection. Idle, Connect and Active belong to a dialer that keeps configured peers up with backoff. - Capabilities are negotiated: address families, four-octet AS numbers with AS_TRANS and AS4_PATH, route refresh, ADD-PATH per family and direction. - Collision detection per RFC 4271 section 6.8, by address and identifier. - Connections are taken only from configured peers, a few per address. - A panic in a session is contained and answered with a Cease; a core task that stops shuts the server down with a Cease to every peer. - Writes that make no progress are given up (RFC 9687). Codec - Malformed UPDATEs are handled as RFC 7606 says: treat-as-withdraw, attribute discard, or session reset, with the NOTIFICATION to send. - Address families are open types; any can be named and ignored. - Prefixes are fixed-size values, normalised, with path identifiers. - Lengths are filled in after writing; one definition of each type code. - Property tests and fuzz targets for every decoder. Routing table - Adj-RIBs-In and Loc-RIB in one sharded table that sessions write to directly. Routes are sixteen bytes; attribute sets are interned in wire form. Best-path selection per RFC 4271 section 9.1.2.2 with deterministic MED. - A journal per shard lets followers read changes at their own pace; one that falls behind is resynchronised. - On a RIPE RIS dump of 148 peers and 39.3M routes: loaded in 2.0 s on 12 cores, about 52 bytes a route. Announcing - Originated routes come from the configuration. Each session has its own exporter with an Adj-RIB-Out, so a slow peer delays only itself. - Routes to a peer in our own AS have an empty path and a LOCAL_PREF. Operations - HTTP status page, JSON API, event stream and Prometheus metrics. - Benchmarks on synthetic and real tables. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
1ec60255ea
commit
630c175918
138 files changed
+21650
-2925
No files matched your search
@@ -0,0 +1,127 @@
|
||||
//! 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, 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::LocalRib;
|
||||
use bgp_rib::table::RibTable;
|
||||
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,
|
||||
/// The routing table: routes learned from peers and the best of them.
|
||||
pub rib: Arc<RibTable>,
|
||||
}
|
||||
|
||||
/// 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(())
|
||||
}
|
||||
Reference in new issue
Block a user