Phase 2 of the teaching demo: everything the status page will need is now reachable over HTTP. - New crate crates/web (axum). JSON endpoints for status, peers, local routes (announce / withdraw), received routes, the exact UPDATE bytes a local route produces per session, and well-known community names. Communities are accepted as ASN:VALUE or by name (NO_EXPORT, BLACKHOLE, ...); large communities as A:B:C. Policy errors map to 403/409/422. - GET /api/events is a server-sent event stream merging session up/down, local route and Adj-RIB-In changes, with lag notifications. - GET /metricz serves Prometheus metrics; the server crate now counts messages sent/received by type and sessions established. - The runnable main moves from crates/server to bin/bgp so it can depend on both server and web without a cycle. `cargo run -- config.textpb`. - Config gains http_endpoint; unset disables the API. - Integration tests drive the API against a live server with a real BGP peer, including verifying the wire view matches the bytes the peer got. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
131 lines
4.2 KiB
Rust
131 lines
4.2 KiB
Rust
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
|
use std::str::FromStr;
|
|
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
use bgp_rib::LocalRibPolicy;
|
|
use eyre::{Result, WrapErr, bail, eyre};
|
|
|
|
use crate::proto::config::BgpConfig;
|
|
|
|
const DEFAULT_BGP_PORT: u16 = 179;
|
|
const DEFAULT_HOLD_TIME_SECS: u16 = 90;
|
|
|
|
/// Validated, Rust-native server configuration derived from the proto config.
|
|
pub struct Config {
|
|
pub router_id: Ipv4Addr,
|
|
pub local_asn: u32,
|
|
/// Default hold time advertised to peers that have no per-peer override.
|
|
pub hold_time_secs: u16,
|
|
pub listen_endpoints: Vec<SocketAddr>,
|
|
pub peers: Vec<PeerConfig>,
|
|
/// Limits on locally originated routes.
|
|
pub local_route_policy: LocalRibPolicy,
|
|
/// Address for the HTTP API; `None` disables it.
|
|
pub http_endpoint: Option<SocketAddr>,
|
|
}
|
|
|
|
pub struct PeerConfig {
|
|
pub name: String,
|
|
pub endpoint: SocketAddr,
|
|
pub remote_asn: u32,
|
|
pub hold_time_secs: u16,
|
|
}
|
|
|
|
impl Config {
|
|
pub fn from_proto(proto: BgpConfig) -> Result<Self> {
|
|
let router_id = Ipv4Addr::from_str(&proto.router_id)
|
|
.map_err(|_| eyre!("Invalid router_id {:?}: must be a dotted-quad IPv4 address", proto.router_id))?;
|
|
|
|
if proto.local_asn == 0 {
|
|
bail!("local_asn must be non-zero");
|
|
}
|
|
|
|
let listen_endpoints = if proto.listen_endpoints.is_empty() {
|
|
vec![
|
|
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), DEFAULT_BGP_PORT),
|
|
SocketAddr::new(IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED), DEFAULT_BGP_PORT),
|
|
]
|
|
} else {
|
|
proto.listen_endpoints
|
|
.into_iter()
|
|
.map(|ep| parse_endpoint(&ep.address, ep.port))
|
|
.collect::<Result<_>>()?
|
|
};
|
|
|
|
let peers = proto.peers
|
|
.into_iter()
|
|
.map(|p| {
|
|
let ep = p.endpoint.as_ref()
|
|
.ok_or_else(|| eyre!("Peer {:?} is missing endpoint", p.name))?;
|
|
let endpoint = parse_endpoint(&ep.address, ep.port)?;
|
|
|
|
if p.remote_asn == 0 {
|
|
bail!("Peer {:?} remote_asn must be non-zero", p.name);
|
|
}
|
|
|
|
let hold_time_secs = if p.hold_time_seconds == 0 {
|
|
DEFAULT_HOLD_TIME_SECS
|
|
} else if p.hold_time_seconds < 3 {
|
|
bail!("Peer {:?} hold_time_seconds must be 0 or >= 3", p.name);
|
|
} else {
|
|
p.hold_time_seconds as u16
|
|
};
|
|
|
|
Ok(PeerConfig {
|
|
name: p.name,
|
|
endpoint,
|
|
remote_asn: p.remote_asn,
|
|
hold_time_secs,
|
|
})
|
|
})
|
|
.collect::<Result<_>>()?;
|
|
|
|
let local_route_policy = match proto.local_route_policy.into_option() {
|
|
Some(policy) => LocalRibPolicy {
|
|
allowed_prefixes: policy
|
|
.allowed_prefixes
|
|
.iter()
|
|
.map(|p| IpPrefix::try_from(p.as_str()).wrap_err_with(|| format!("Invalid allowed prefix {p:?}")))
|
|
.collect::<Result<_>>()?,
|
|
max_routes_per_owner: policy.max_routes_per_owner as usize,
|
|
},
|
|
None => LocalRibPolicy::default(),
|
|
};
|
|
|
|
let http_endpoint = match proto.http_endpoint.into_option() {
|
|
Some(ep) => {
|
|
if ep.port == 0 {
|
|
bail!("http_endpoint.port is required");
|
|
}
|
|
Some(parse_endpoint(&ep.address, ep.port).wrap_err("Invalid http_endpoint")?)
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
Ok(Self {
|
|
router_id,
|
|
local_asn: proto.local_asn,
|
|
hold_time_secs: DEFAULT_HOLD_TIME_SECS,
|
|
listen_endpoints,
|
|
peers,
|
|
local_route_policy,
|
|
http_endpoint,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn parse_endpoint(address: &str, port: u32) -> Result<SocketAddr> {
|
|
let port = if port == 0 {
|
|
DEFAULT_BGP_PORT
|
|
} else if port > u16::MAX as u32 {
|
|
bail!("Port {port} is out of range");
|
|
} else {
|
|
port as u16
|
|
};
|
|
|
|
let ip = IpAddr::from_str(address)
|
|
.map_err(|_| eyre!("Invalid IP address {address:?}"))?;
|
|
|
|
Ok(SocketAddr::new(ip, port))
|
|
}
|