//! Locally originated routes (the "Loc-RIB" of routes this router announces //! on its own behalf, as opposed to routes learned from peers). //! //! The actor stores one route per prefix together with the attributes chosen //! by whoever announced it. It does not know about peers or sessions: the //! server subscribes to [`LocalRibEvent`]s and turns them into UPDATEs. use std::collections::HashMap; use std::time::SystemTime; use bgp_packet::message::{LargeCommunity, OriginPathAttribute}; use bgp_primitives::ip_prefix::IpPrefix; use serde::{Deserialize, Serialize}; use tokio::sync::{broadcast, mpsc, oneshot}; use tracing::warn; const EVENT_CHANNEL_CAPACITY: usize = 1024; const CMD_CHANNEL_CAPACITY: usize = 256; /// User-selectable attributes for a locally originated route. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct LocalRouteAttrs { pub origin: OriginPathAttribute, /// Number of *additional* copies of the local ASN to prepend to the /// AS_PATH. `0` yields the plain `[local_asn]` path. pub prepend: u8, pub med: Option, /// RFC 1997 standard communities as `(high, low)` 16-bit halves. pub communities: Vec<(u16, u16)>, /// RFC 8092 large communities. pub large_communities: Vec, } impl Default for LocalRouteAttrs { fn default() -> Self { Self { origin: OriginPathAttribute::IGP, prepend: 0, med: None, communities: vec![], large_communities: vec![], } } } /// A route originated by this router. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct LocalRoute { pub prefix: IpPrefix, /// Free-form identity of whoever announced the route (e.g. a student /// name). Used to stop one owner replacing or withdrawing another's route. pub owner: String, pub attrs: LocalRouteAttrs, pub announced_at: SystemTime, } impl LocalRoute { pub fn new(prefix: IpPrefix, owner: impl Into, attrs: LocalRouteAttrs) -> Self { Self { prefix, owner: owner.into(), attrs, announced_at: SystemTime::now() } } } /// Limits on what may be announced. Enforced inside the actor so every caller /// (HTTP API, tests, future CLI) goes through the same checks. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct LocalRibPolicy { /// A route is accepted only if it is covered by at least one of these /// prefixes. Empty means no restriction. pub allowed_prefixes: Vec, /// Maximum number of routes a single owner may hold. `0` means unlimited. pub max_routes_per_owner: usize, } impl LocalRibPolicy { fn allows(&self, prefix: &IpPrefix) -> bool { self.allowed_prefixes.is_empty() || self.allowed_prefixes.iter().any(|p| p.contains(prefix)) } } #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum LocalRibError { #[error("prefix {0} is not covered by the allowed prefixes")] NotAllowed(IpPrefix), #[error("owner {owner:?} already has {limit} routes")] OwnerLimit { owner: String, limit: usize }, #[error("prefix {prefix} is owned by {owner:?}")] OwnedByOther { prefix: IpPrefix, owner: String }, #[error("local RIB actor has stopped")] Closed, } #[derive(Debug, Clone)] pub enum LocalRibEvent { /// A route was added or its attributes replaced. Announced(LocalRoute), Withdrawn { prefix: IpPrefix }, } enum Command { Announce { route: LocalRoute, reply: oneshot::Sender> }, Withdraw { prefix: IpPrefix, requester: Option, reply: oneshot::Sender, LocalRibError>>, }, Snapshot { reply: oneshot::Sender> }, } /// Handle to the local RIB actor. Cheap to clone; all clones share one table. #[derive(Clone)] pub struct LocalRib { tx: mpsc::Sender, events: broadcast::Sender, } impl LocalRib { pub fn spawn(policy: LocalRibPolicy) -> Self { let (tx, rx) = mpsc::channel(CMD_CHANNEL_CAPACITY); let (events, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY); tokio::spawn(run_actor(rx, events.clone(), policy)); Self { tx, events } } /// Adds a route, or replaces the attributes of an existing route with the /// same prefix and owner. pub async fn announce(&self, route: LocalRoute) -> Result<(), LocalRibError> { let (reply, rx) = oneshot::channel(); self.tx .send(Command::Announce { route, reply }) .await .map_err(|_| LocalRibError::Closed)?; rx.await.map_err(|_| LocalRibError::Closed)? } /// Removes a route. `requester` must match the route's owner; pass `None` /// to withdraw regardless of owner (administrative override). Returns the /// removed route, or `None` if the prefix was not present. pub async fn withdraw( &self, prefix: IpPrefix, requester: Option<&str>, ) -> Result, LocalRibError> { let (reply, rx) = oneshot::channel(); self.tx .send(Command::Withdraw { prefix, requester: requester.map(str::to_owned), reply }) .await .map_err(|_| LocalRibError::Closed)?; rx.await.map_err(|_| LocalRibError::Closed)? } /// Point-in-time copy of every route, in no particular order. pub async fn snapshot(&self) -> Vec { let (reply, rx) = oneshot::channel(); let _ = self.tx.send(Command::Snapshot { reply }).await; rx.await.unwrap_or_default() } /// Subscribe to mutations. See [`crate::AdjRibIn::subscribe`] for lag /// semantics. pub fn subscribe(&self) -> broadcast::Receiver { self.events.subscribe() } } async fn run_actor( mut rx: mpsc::Receiver, events: broadcast::Sender, policy: LocalRibPolicy, ) { let mut table: HashMap = HashMap::new(); while let Some(cmd) = rx.recv().await { match cmd { Command::Announce { route, reply } => { let result = check_announce(&table, &policy, &route).map(|()| { table.insert(route.prefix.clone(), route.clone()); let _ = events.send(LocalRibEvent::Announced(route)); }); let _ = reply.send(result); } Command::Withdraw { prefix, requester, reply } => { let result = match (table.get(&prefix), requester) { (Some(existing), Some(req)) if existing.owner != req => { Err(LocalRibError::OwnedByOther { prefix: prefix.clone(), owner: existing.owner.clone(), }) } _ => { let removed = table.remove(&prefix); if removed.is_some() { let _ = events.send(LocalRibEvent::Withdrawn { prefix }); } Ok(removed) } }; let _ = reply.send(result); } Command::Snapshot { reply } => { if reply.send(table.values().cloned().collect()).is_err() { warn!("Snapshot reply channel closed before send"); } } } } } fn check_announce( table: &HashMap, policy: &LocalRibPolicy, route: &LocalRoute, ) -> Result<(), LocalRibError> { if !policy.allows(&route.prefix) { return Err(LocalRibError::NotAllowed(route.prefix.clone())); } if let Some(existing) = table.get(&route.prefix) { if existing.owner != route.owner { return Err(LocalRibError::OwnedByOther { prefix: route.prefix.clone(), owner: existing.owner.clone(), }); } // Replacing our own route never changes the per-owner count. return Ok(()); } if policy.max_routes_per_owner > 0 { let held = table.values().filter(|r| r.owner == route.owner).count(); if held >= policy.max_routes_per_owner { return Err(LocalRibError::OwnerLimit { owner: route.owner.clone(), limit: policy.max_routes_per_owner, }); } } Ok(()) } #[cfg(test)] mod tests { use super::*; fn prefix(s: &str) -> IpPrefix { IpPrefix::try_from(s).unwrap() } fn route(p: &str, owner: &str) -> LocalRoute { LocalRoute::new(prefix(p), owner, LocalRouteAttrs::default()) } #[tokio::test] async fn announce_and_withdraw_emit_events() { let rib = LocalRib::spawn(LocalRibPolicy::default()); let mut events = rib.subscribe(); rib.announce(route("10.0.0.0/24", "alice")).await.unwrap(); assert!(matches!(events.recv().await.unwrap(), LocalRibEvent::Announced(r) if r.prefix == prefix("10.0.0.0/24"))); assert_eq!(rib.snapshot().await.len(), 1); let removed = rib.withdraw(prefix("10.0.0.0/24"), Some("alice")).await.unwrap(); assert_eq!(removed.unwrap().owner, "alice"); assert!(matches!(events.recv().await.unwrap(), LocalRibEvent::Withdrawn { prefix: p } if p == prefix("10.0.0.0/24"))); assert!(rib.snapshot().await.is_empty()); // Withdrawing an absent prefix is a no-op, not an error. assert_eq!(rib.withdraw(prefix("10.0.0.0/24"), None).await.unwrap(), None); } #[tokio::test] async fn policy_allowlist_is_enforced() { let rib = LocalRib::spawn(LocalRibPolicy { allowed_prefixes: vec![prefix("10.100.0.0/16")], max_routes_per_owner: 0, }); rib.announce(route("10.100.3.0/24", "alice")).await.unwrap(); let err = rib.announce(route("192.0.2.0/24", "alice")).await.unwrap_err(); assert_eq!(err, LocalRibError::NotAllowed(prefix("192.0.2.0/24"))); } #[tokio::test] async fn owner_limit_ignores_replacements() { let rib = LocalRib::spawn(LocalRibPolicy { allowed_prefixes: vec![], max_routes_per_owner: 1 }); rib.announce(route("10.0.0.0/24", "alice")).await.unwrap(); // Replacing the same prefix is fine. rib.announce(route("10.0.0.0/24", "alice")).await.unwrap(); let err = rib.announce(route("10.0.1.0/24", "alice")).await.unwrap_err(); assert!(matches!(err, LocalRibError::OwnerLimit { limit: 1, .. })); // Other owners are unaffected. rib.announce(route("10.0.1.0/24", "bob")).await.unwrap(); } #[tokio::test] async fn other_owner_cannot_replace_or_withdraw() { let rib = LocalRib::spawn(LocalRibPolicy::default()); rib.announce(route("10.0.0.0/24", "alice")).await.unwrap(); let err = rib.announce(route("10.0.0.0/24", "bob")).await.unwrap_err(); assert!(matches!(err, LocalRibError::OwnedByOther { .. })); let err = rib.withdraw(prefix("10.0.0.0/24"), Some("bob")).await.unwrap_err(); assert!(matches!(err, LocalRibError::OwnedByOther { .. })); // Administrative override works. assert!(rib.withdraw(prefix("10.0.0.0/24"), None).await.unwrap().is_some()); } }