From b9adc68eb80b902ae814bf78170d411299a501b1 Mon Sep 17 00:00:00 2001 From: Renato Westphal Date: Sun, 22 Dec 2024 20:01:02 -0300 Subject: [PATCH] isis: implement the SPF algorithm and route computation This commit implements the following: * SPF computation (both full runs and partial runs) * Route computation for both IPv4 and IPv6 address families * Installation of IS-IS routes in the global RIB * Support for both old-style (narrow) and wide metrics Of particular note, this implementation deviates slightly from the ISO standard. Specifically, we don't pre-load TENT with the local adjacency database. Instead, we pre-load from the local LSP. This was done to allow SPF to be run with any router as the root, which will be necessary in the future for the implementation of the TI-LFA feature. Next steps: * Conformance tests * Implement multi-topology support (RFC 5120) * Implement domain-wide prefix distribution (RFC 5302) Signed-off-by: Renato Westphal --- holo-isis/src/collections.rs | 15 +- holo-isis/src/debug.rs | 46 ++- holo-isis/src/instance.rs | 14 +- holo-isis/src/lib.rs | 1 + holo-isis/src/lsdb.rs | 17 +- holo-isis/src/northbound/state.rs | 49 ++- holo-isis/src/packet/mod.rs | 18 + holo-isis/src/packet/tlv.rs | 37 +- holo-isis/src/route.rs | 173 +++++++++ holo-isis/src/southbound/tx.rs | 61 ++++ holo-isis/src/spf.rs | 571 +++++++++++++++++++++++++++++- 11 files changed, 954 insertions(+), 48 deletions(-) create mode 100644 holo-isis/src/route.rs diff --git a/holo-isis/src/collections.rs b/holo-isis/src/collections.rs index 53e85d20..2dff2959 100644 --- a/holo-isis/src/collections.rs +++ b/holo-isis/src/collections.rs @@ -18,7 +18,7 @@ use crate::error::Error; use crate::interface::Interface; use crate::lsdb::LspEntry; use crate::packet::pdu::Lsp; -use crate::packet::{LevelNumber, LevelType, LspId, SystemId}; +use crate::packet::{LanId, LevelNumber, LevelType, LspId, SystemId}; use crate::tasks::messages::input::LspPurgeMsg; pub type ObjectId = u32; @@ -638,6 +638,19 @@ impl Lsdb { self.range(arena, start..=end) } + // Returns an iterator visiting all LSP entries for the specified LAN ID. + // + // LSP are ordered by their LSP IDs. + pub(crate) fn iter_for_lan_id<'a>( + &'a self, + arena: &'a Arena, + lan_id: LanId, + ) -> impl Iterator + 'a { + let start = LspId::from((lan_id, 0)); + let end = LspId::from((lan_id, 255)); + self.range(arena, start..=end) + } + // Returns an iterator over a range of LSP IDs. // // LSP are ordered by their LSP IDs. diff --git a/holo-isis/src/debug.rs b/holo-isis/src/debug.rs index ee69eb88..688bbe6f 100644 --- a/holo-isis/src/debug.rs +++ b/holo-isis/src/debug.rs @@ -7,6 +7,7 @@ // See: https://nlnet.nl/NGI0 // +use holo_utils::ip::AddressFamily; use holo_yang::ToYang; use serde::{Deserialize, Serialize}; use tracing::{debug, debug_span}; @@ -17,6 +18,7 @@ use crate::network::MulticastAddr; use crate::packet::pdu::{Lsp, Pdu}; use crate::packet::LevelNumber; use crate::spf; +use crate::spf::{Vertex, VertexEdge}; // IS-IS debug messages. #[derive(Debug)] @@ -48,8 +50,11 @@ pub enum Debug<'a> { LspDelete(LevelNumber, &'a Lsp), LspRefresh(LevelNumber, &'a Lsp), // SPF - SpfDelayFsmEvent(LevelNumber, spf::fsm::State, spf::fsm::Event), - SpfDelayFsmTransition(LevelNumber, spf::fsm::State, spf::fsm::State), + SpfDelayFsmEvent(spf::fsm::State, spf::fsm::Event), + SpfDelayFsmTransition(spf::fsm::State, spf::fsm::State), + SpfMaxPathMetric(&'a Vertex, &'a VertexEdge, u32), + SpfMissingProtocolsTlv(&'a Vertex), + SpfUnsupportedProtocol(&'a Vertex, AddressFamily), } // Reason why an IS-IS instance is inactive. @@ -163,13 +168,25 @@ impl Debug<'_> { // Parent span(s): isis-instance debug!(?level, lsp_id = %lsp.lsp_id.to_yang(), seqno = %lsp.seqno, len = %lsp.raw.len(), ?reason, "{}", self); } - Debug::SpfDelayFsmEvent(level, state, event) => { - // Parent span(s): isis-instance - debug!(?level, ?state, ?event, "{}", self); + Debug::SpfDelayFsmEvent(state, event) => { + // Parent span(s): isis-instance:spf + debug!(?state, ?event, "{}", self); } - Debug::SpfDelayFsmTransition(level, old_state, new_state) => { - // Parent span(s): isis-instance - debug!(?level, ?old_state, ?new_state, "{}", self); + Debug::SpfDelayFsmTransition(old_state, new_state) => { + // Parent span(s): isis-instance:spf + debug!(?old_state, ?new_state, "{}", self); + } + Debug::SpfMaxPathMetric(vertex, link, distance) => { + // Parent span(s): isis-instance:spf + debug!(vertex = %vertex.id.lan_id.to_yang(), link = %link.id.lan_id.to_yang(), %distance, "{}", self); + } + Debug::SpfMissingProtocolsTlv(vertex) => { + // Parent span(s): isis-instance:spf + debug!(vertex = %vertex.id.lan_id.to_yang(), "{}", self); + } + Debug::SpfUnsupportedProtocol(vertex, protocol) => { + // Parent span(s): isis-instance:spf + debug!(vertex = %vertex.id.lan_id.to_yang(), %protocol, "{}", self); } } } @@ -236,10 +253,19 @@ impl std::fmt::Display for Debug<'_> { write!(f, "refreshing LSP") } Debug::SpfDelayFsmEvent(..) => { - write!(f, "SPF Delay FSM event") + write!(f, "delay FSM event") } Debug::SpfDelayFsmTransition(..) => { - write!(f, "SPF Delay FSM state transition") + write!(f, "delay FSM state transition") + } + Debug::SpfMaxPathMetric(..) => { + write!(f, "maximum path metric exceeded") + } + Debug::SpfMissingProtocolsTlv(..) => { + write!(f, "missing protocols TLV") + } + Debug::SpfUnsupportedProtocol(..) => { + write!(f, "unsupported protocol") } } } diff --git a/holo-isis/src/instance.rs b/holo-isis/src/instance.rs index 51d12b5f..0f0598a5 100644 --- a/holo-isis/src/instance.rs +++ b/holo-isis/src/instance.rs @@ -7,7 +7,7 @@ // See: https://nlnet.nl/NGI0 // -use std::collections::VecDeque; +use std::collections::{BTreeMap, VecDeque}; use std::net::Ipv4Addr; use std::time::Instant; @@ -20,6 +20,7 @@ use holo_utils::ibus::IbusMsg; use holo_utils::protocol::Protocol; use holo_utils::task::TimeoutTask; use holo_utils::{Receiver, Sender, UnboundedReceiver, UnboundedSender}; +use ipnetwork::IpNetwork; use tokio::sync::mpsc; use crate::adjacency::Adjacency; @@ -32,7 +33,8 @@ use crate::interface::CircuitIdAllocator; use crate::lsdb::{LspEntry, LspLogEntry}; use crate::northbound::configuration::InstanceCfg; use crate::packet::{LevelNumber, LevelType, Levels}; -use crate::spf::{SpfLogEntry, SpfScheduler}; +use crate::route::Route; +use crate::spf::{SpfLogEntry, SpfScheduler, Vertex, VertexId}; use crate::tasks::messages::input::{ AdjHoldTimerMsg, DisElectionMsg, LspDeleteMsg, LspOriginateMsg, LspPurgeMsg, LspRefreshMsg, NetRxPduMsg, SendCsnpMsg, SendPsnpMsg, @@ -77,6 +79,11 @@ pub struct InstanceState { pub lsp_orig_pending: Option, // SPF scheduler state. pub spf_sched: Levels, + // Shortest-path tree. + pub spt: Levels>, + // Routing table (per-level and L1/L2). + pub rib_single: Levels>, + pub rib_multi: BTreeMap, // Event counters. pub counters: Levels, pub discontinuity_time: DateTime, @@ -377,6 +384,9 @@ impl InstanceState { lsp_orig_backoff: None, lsp_orig_pending: None, spf_sched: Default::default(), + spt: Default::default(), + rib_single: Default::default(), + rib_multi: Default::default(), counters: Default::default(), discontinuity_time: Utc::now(), lsp_log: Default::default(), diff --git a/holo-isis/src/lib.rs b/holo-isis/src/lib.rs index 2af67cbd..4827d60a 100644 --- a/holo-isis/src/lib.rs +++ b/holo-isis/src/lib.rs @@ -24,6 +24,7 @@ pub mod lsdb; pub mod network; pub mod northbound; pub mod packet; +pub mod route; pub mod southbound; pub mod spf; pub mod tasks; diff --git a/holo-isis/src/lsdb.rs b/holo-isis/src/lsdb.rs index 70edfa63..08854654 100644 --- a/holo-isis/src/lsdb.rs +++ b/holo-isis/src/lsdb.rs @@ -29,6 +29,7 @@ use crate::packet::tlv::{ ExtIpv4Reach, ExtIsReach, Ipv4Reach, Ipv6Reach, IsReach, Nlpid, }; use crate::packet::{LanId, LevelNumber, LspId}; +use crate::spf::SpfType; use crate::tasks::messages::input::LspPurgeMsg; use crate::{spf, tasks}; @@ -454,10 +455,18 @@ pub(crate) fn install<'a>( // Check if the LSP content has changed. let mut content_change = true; + let mut topology_change = true; if let Some(old_lsp) = old_lsp - && (old_lsp.flags == lsp.flags && old_lsp.tlvs == lsp.tlvs) + && lsp.flags == old_lsp.flags { - content_change = false; + if old_lsp.tlvs == lsp.tlvs { + content_change = false; + topology_change = false; + } else if old_lsp.tlvs.is_reach().eq(lsp.tlvs.is_reach()) + && old_lsp.tlvs.ext_is_reach().eq(lsp.tlvs.ext_is_reach()) + { + topology_change = false; + } } // Add LSP entry to LSDB. @@ -483,6 +492,10 @@ pub(crate) fn install<'a>( let spf_sched = instance.state.spf_sched.get_mut(level); spf_sched.trigger_lsps.push(lsp_log_id); spf_sched.schedule_time.get_or_insert_with(Instant::now); + if topology_change { + spf_sched.spf_type = SpfType::Full; + } + instance .tx .protocol_input diff --git a/holo-isis/src/northbound/state.rs b/holo-isis/src/northbound/state.rs index 1a968b8a..61c18fb1 100644 --- a/holo-isis/src/northbound/state.rs +++ b/holo-isis/src/northbound/state.rs @@ -20,6 +20,7 @@ use holo_northbound::state::{ use holo_northbound::yang::control_plane_protocol::isis; use holo_utils::option::OptionExt; use holo_yang::{ToYang, ToYangBits}; +use ipnetwork::IpNetwork; use crate::adjacency::Adjacency; use crate::collections::Lsdb; @@ -29,7 +30,8 @@ use crate::lsdb::{LspEntry, LspLogEntry, LspLogId}; use crate::packet::tlv::{ ExtIpv4Reach, ExtIsReach, Ipv4Reach, Ipv6Reach, IsReach, UnknownTlv, }; -use crate::packet::{LanId, LevelNumber}; +use crate::packet::{LanId, LevelNumber, LevelType}; +use crate::route::{Nexthop, Route}; use crate::spf::SpfLogEntry; pub static CALLBACKS: Lazy> = Lazy::new(load_callbacks); @@ -51,6 +53,8 @@ pub enum ListEntry<'a> { ExtIpv4Reach(&'a ExtIpv4Reach), Ipv6Reach(&'a Ipv6Reach), UnknownTlv(&'a UnknownTlv), + Route(&'a IpNetwork, &'a Route), + Nexthop(&'a Nexthop), SystemCounters(LevelNumber), Interface(&'a Interface), InterfacePacketCounters(&'a Interface, LevelNumber), @@ -531,29 +535,44 @@ fn load_callbacks() -> Callbacks { }) }) .path(isis::local_rib::route::PATH) - .get_iterate(|_instance, _args| { - // TODO: implement me! - None + .get_iterate(|instance, _args| { + let Some(instance_state) = &instance.state else { return None }; + match instance.config.level_type { + LevelType::L1 | LevelType::L2 => { + let iter = instance_state.rib_single.get(instance.config.level_type).iter(); + let iter = iter.map(|(destination, route)| ListEntry::Route(destination, route)); + Some(Box::new(iter)) + } + LevelType::All => { + let iter = instance_state.rib_multi.iter(); + let iter = iter.map(|(destination, route)| ListEntry::Route(destination, route)); + Some(Box::new(iter)) + } + } }) - .get_object(|_instance, _args| { + .get_object(|_instance, args| { use isis::local_rib::route::Route; + let (prefix, route) = args.list_entry.as_route().unwrap(); Box::new(Route { - prefix: todo!(), - metric: None, - level: None, - route_tag: None, + prefix: Cow::Borrowed(prefix), + metric: Some(route.metric), + level: Some(route.level as u8), + route_tag: route.tag, }) }) .path(isis::local_rib::route::next_hops::next_hop::PATH) - .get_iterate(|_instance, _args| { - // TODO: implement me! - None + .get_iterate(|_instance, args| { + let (_, route) = args.parent_list_entry.as_route().unwrap(); + let iter = route.nexthops.values().map(ListEntry::Nexthop); + Some(Box::new(iter)) }) - .get_object(|_instance, _args| { + .get_object(|instance, args| { use isis::local_rib::route::next_hops::next_hop::NextHop; + let nexthop = args.list_entry.as_nexthop().unwrap(); + let iface = &instance.arenas.interfaces[nexthop.iface_idx]; Box::new(NextHop { - next_hop: todo!(), - outgoing_interface: None, + next_hop: Cow::Borrowed(&nexthop.addr), + outgoing_interface: Some(Cow::Borrowed(iface.name.as_str())), }) }) .path(isis::system_counters::level::PATH) diff --git a/holo-isis/src/packet/mod.rs b/holo-isis/src/packet/mod.rs index 0c2fb850..a181271a 100644 --- a/holo-isis/src/packet/mod.rs +++ b/holo-isis/src/packet/mod.rs @@ -227,6 +227,10 @@ impl LanId { self.system_id.encode(buf); buf.put_u8(self.pseudonode); } + + pub(crate) const fn is_pseudonode(&self) -> bool { + self.pseudonode != 0 + } } impl From<[u8; 7]> for LanId { @@ -263,6 +267,10 @@ impl LspId { buf.put_u8(self.pseudonode); buf.put_u8(self.fragment); } + + pub(crate) const fn is_pseudonode(&self) -> bool { + self.pseudonode != 0 + } } impl From<[u8; 8]> for LspId { @@ -286,3 +294,13 @@ impl From<(SystemId, u8, u8)> for LspId { } } } + +impl From<(LanId, u8)> for LspId { + fn from(components: (LanId, u8)) -> LspId { + LspId { + system_id: components.0.system_id, + pseudonode: components.0.pseudonode, + fragment: components.1, + } + } +} diff --git a/holo-isis/src/packet/tlv.rs b/holo-isis/src/packet/tlv.rs index e4c7b42a..d17ec1bc 100644 --- a/holo-isis/src/packet/tlv.rs +++ b/holo-isis/src/packet/tlv.rs @@ -14,7 +14,7 @@ use std::net::{Ipv4Addr, Ipv6Addr}; use bytes::{Buf, BufMut, Bytes, BytesMut}; use derive_new::new; use holo_utils::bytes::{BytesExt, BytesMutExt}; -use holo_utils::ip::{Ipv4AddrExt, Ipv6AddrExt}; +use holo_utils::ip::{AddressFamily, Ipv4AddrExt, Ipv6AddrExt}; use ipnetwork::{Ipv4Network, Ipv6Network}; use num_traits::{FromPrimitive, ToPrimitive}; use serde::{Deserialize, Serialize}; @@ -215,6 +215,17 @@ pub struct UnknownTlv { pub value: Bytes, } +// ===== impl Nlpid ===== + +impl From for Nlpid { + fn from(af: AddressFamily) -> Nlpid { + match af { + AddressFamily::Ipv4 => Nlpid::Ipv4, + AddressFamily::Ipv6 => Nlpid::Ipv6, + } + } +} + // ===== impl AreaAddressesTlv ===== impl AreaAddressesTlv { @@ -367,6 +378,10 @@ impl ProtocolsSupportedTlv { } tlv_encode_end(buf, start_pos); } + + pub(crate) fn contains(&self, protocol: Nlpid) -> bool { + self.list.contains(&(protocol as u8)) + } } impl Tlv for ProtocolsSupportedTlv { @@ -820,6 +835,26 @@ where } } +// ===== impl Ipv4Reach ===== + +impl Ipv4Reach { + // Returns the metric associated with the IP prefix. + pub(crate) fn metric(&self) -> u32 { + let mut metric = self.metric; + + // RFC 3787 - Section 5: + // "We interpret the default metric as an 7 bit quantity. Metrics + // with the external bit set are interpreted as metrics in the range + // [64..127]. Metrics with the external bit clear are interpreted as + // metrics in the range [0..63]". + if self.ie_bit { + metric += 64; + } + + metric.into() + } +} + // ===== impl ExtIpv4ReachTlv ===== impl ExtIpv4ReachTlv { diff --git a/holo-isis/src/route.rs b/holo-isis/src/route.rs new file mode 100644 index 00000000..76c2d352 --- /dev/null +++ b/holo-isis/src/route.rs @@ -0,0 +1,173 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// + +use std::collections::BTreeMap; +use std::net::IpAddr; + +use bitflags::bitflags; +use derive_new::new; +use holo_utils::ip::{AddressFamily, IpNetworkKind}; +use holo_utils::southbound::IsisRouteType; +use ipnetwork::IpNetwork; + +use crate::collections::{InterfaceIndex, Interfaces}; +use crate::instance::InstanceUpView; +use crate::northbound::configuration::InstanceCfg; +use crate::packet::LevelNumber; +use crate::southbound; +use crate::spf::{Vertex, VertexNetwork}; + +// Routing table entry. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Route { + pub route_type: IsisRouteType, + pub metric: u32, + pub level: LevelNumber, + pub tag: Option, + pub nexthops: BTreeMap, + pub flags: RouteFlags, +} + +bitflags! { + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] + pub struct RouteFlags: u8 { + const CONNECTED = 0x01; + const INSTALLED = 0x02; + } +} + +// Route nexthop. +#[derive(Clone, Copy, Debug, Eq, new, PartialEq)] +pub struct Nexthop { + // Nexthop interface. + pub iface_idx: InterfaceIndex, + // Nexthop address (`None` for connected routes). + pub addr: IpAddr, +} + +// ===== impl Route ===== + +impl Route { + pub(crate) fn new( + vertex: &Vertex, + vertex_network: &VertexNetwork, + level: LevelNumber, + ) -> Route { + let mut flags = RouteFlags::empty(); + if vertex.hops == 0 { + flags.insert(RouteFlags::CONNECTED); + } + let route_type = match (level, vertex_network.external) { + (LevelNumber::L1, false) => IsisRouteType::L1IntraArea, + (LevelNumber::L1, true) => IsisRouteType::L1External, + (LevelNumber::L2, false) => IsisRouteType::L2IntraArea, + (LevelNumber::L2, true) => IsisRouteType::L2External, + }; + Route { + route_type, + metric: vertex.distance + vertex_network.metric, + level, + tag: None, + nexthops: Self::build_nexthops(vertex, vertex_network), + flags, + } + } + + pub(crate) fn merge_nexthops( + &mut self, + vertex: &Vertex, + vertex_network: &VertexNetwork, + ) { + let nexthops = Self::build_nexthops(vertex, vertex_network); + self.nexthops.extend(nexthops); + } + + fn build_nexthops( + vertex: &Vertex, + vertex_network: &VertexNetwork, + ) -> BTreeMap { + vertex + .nexthops + .iter() + .filter_map(|nexthop| { + let iface_idx = nexthop.iface_idx; + let addr = match vertex_network.prefix.address_family() { + AddressFamily::Ipv4 => nexthop.ipv4.map(IpAddr::V4), + AddressFamily::Ipv6 => nexthop.ipv6.map(IpAddr::V6), + }?; + Some((addr, Nexthop { iface_idx, addr })) + }) + .collect() + } + + pub(crate) const fn distance(&self, config: &InstanceCfg) -> u8 { + match self.route_type { + IsisRouteType::L2IntraArea + | IsisRouteType::L1IntraArea + | IsisRouteType::L1InterArea => config.preference.internal, + IsisRouteType::L2External + | IsisRouteType::L1External + | IsisRouteType::L1InterAreaExternal => config.preference.external, + } + } +} + +// ===== global functions ===== + +// Updates IS-IS routes in the global RIB. +pub(crate) fn update_global_rib( + rib: &mut BTreeMap, + mut old_rib: BTreeMap, + instance: &mut InstanceUpView<'_>, + interfaces: &Interfaces, +) { + // Install new routes or routes that have changed. + // + // TODO: prioritize loopback routes to speedup BGP convergence. + for (prefix, route) in rib { + // Remove route from the old RIB if it's present. + if let Some(old_route) = old_rib.remove(prefix) { + // Skip reinstalling the route if it hasn't changed. + if old_route.metric == route.metric + && old_route.tag == route.tag + && old_route.nexthops == route.nexthops + { + if old_route.flags.contains(RouteFlags::INSTALLED) { + route.flags.insert(RouteFlags::INSTALLED); + } + continue; + } + } + + // The list of nexthops might be empty in the case of nexthop + // computation errors (e.g. adjacencies with missing IP address TLVs). + // When that happens, ensure the route is removed from the global RIB. + if !route.flags.contains(RouteFlags::CONNECTED) + && !route.nexthops.is_empty() + { + let distance = route.distance(instance.config); + southbound::tx::route_install( + &instance.tx.ibus, + prefix, + route, + distance, + interfaces, + ); + route.flags.insert(RouteFlags::INSTALLED); + } else if route.flags.contains(RouteFlags::INSTALLED) { + southbound::tx::route_uninstall(&instance.tx.ibus, prefix, route); + route.flags.remove(RouteFlags::INSTALLED); + } + } + + // Uninstall routes that are no longer available. + for (dest, route) in old_rib + .into_iter() + .filter(|(_, route)| route.flags.contains(RouteFlags::INSTALLED)) + { + southbound::tx::route_uninstall(&instance.tx.ibus, &dest, &route); + } +} diff --git a/holo-isis/src/southbound/tx.rs b/holo-isis/src/southbound/tx.rs index 11dc1a57..5f63bdc9 100644 --- a/holo-isis/src/southbound/tx.rs +++ b/holo-isis/src/southbound/tx.rs @@ -7,10 +7,71 @@ // See: https://nlnet.nl/NGI0 // +use std::collections::BTreeSet; + use holo_utils::ibus::{IbusMsg, IbusSender}; +use holo_utils::protocol::Protocol; +use holo_utils::southbound::{ + Nexthop, RouteKeyMsg, RouteMsg, RouteOpaqueAttrs, +}; +use ipnetwork::IpNetwork; + +use crate::collections::Interfaces; +use crate::route::Route; // ===== global functions ===== pub(crate) fn router_id_query(ibus_tx: &IbusSender) { let _ = ibus_tx.send(IbusMsg::RouterIdQuery); } + +pub(crate) fn route_install( + ibus_tx: &IbusSender, + destination: &IpNetwork, + route: &Route, + distance: u8, + interfaces: &Interfaces, +) { + // Fill-in nexthops. + let nexthops = route + .nexthops + .values() + .map(|nexthop| { + let iface = &interfaces[nexthop.iface_idx]; + Nexthop::Address { + ifindex: iface.system.ifindex.unwrap(), + addr: nexthop.addr, + labels: vec![], + } + }) + .collect::>(); + + // Install route. + let msg = RouteMsg { + protocol: Protocol::ISIS, + prefix: *destination, + distance: distance.into(), + metric: route.metric, + tag: route.tag, + opaque_attrs: RouteOpaqueAttrs::Isis { + route_type: route.route_type, + }, + nexthops: nexthops.clone(), + }; + let msg = IbusMsg::RouteIpAdd(msg); + let _ = ibus_tx.send(msg); +} + +pub(crate) fn route_uninstall( + ibus_tx: &IbusSender, + destination: &IpNetwork, + _route: &Route, +) { + // Uninstall route. + let msg = RouteKeyMsg { + protocol: Protocol::ISIS, + prefix: *destination, + }; + let msg = IbusMsg::RouteIpDel(msg); + let _ = ibus_tx.send(msg); +} diff --git a/holo-isis/src/spf.rs b/holo-isis/src/spf.rs index 0cf85b45..3f540690 100644 --- a/holo-isis/src/spf.rs +++ b/holo-isis/src/spf.rs @@ -7,30 +7,104 @@ // See: https://nlnet.nl/NGI0 // +use std::cmp::Ordering; +use std::collections::{btree_map, BTreeMap, BTreeSet}; +use std::net::{Ipv4Addr, Ipv6Addr}; use std::time::{Duration, Instant}; use chrono::Utc; use derive_new::new; +use holo_utils::ip::AddressFamily; use holo_utils::task::TimeoutTask; +use ipnetwork::IpNetwork; +use tracing::debug_span; -use crate::adjacency::Adjacency; -use crate::collections::{Arena, Interfaces}; +use crate::adjacency::{Adjacency, AdjacencyState}; +use crate::collections::{Arena, InterfaceIndex, Interfaces, Lsdb}; use crate::debug::Debug; use crate::error::Error; use crate::instance::{InstanceArenas, InstanceUpView}; +use crate::interface::InterfaceType; use crate::lsdb::{LspEntry, LspLogId}; -use crate::packet::LevelNumber; -use crate::tasks; +use crate::northbound::configuration::MetricType; +use crate::packet::consts::LspFlags; +use crate::packet::pdu::Lsp; +use crate::packet::tlv::Nlpid; +use crate::packet::{LanId, LevelNumber, LevelType, LspId, SystemId}; +use crate::route::Route; +use crate::{route, tasks}; // Maximum size of the SPF log record. const SPF_LOG_MAX_SIZE: usize = 32; // Maximum number of trigger LSPs per entry in the SPF log record. const SPF_LOG_TRIGGER_LSPS_MAX_SIZE: usize = 8; +// Maximum total metric value for a complete path (standard metrics). +const MAX_PATH_METRIC_STANDARD: u32 = 1023; +// Maximum total metric value for a complete path (wide metrics). +const MAX_PATH_METRIC_WIDE: u32 = 0xFE000000; +// Represents a vertex in the IS-IS topology graph. +// +// A `Vertex` corresponds to a router or pseudonode. +#[derive(Debug)] +#[derive(new)] +pub struct Vertex { + pub id: VertexId, + pub distance: u32, + pub hops: u16, + #[new(default)] + pub nexthops: Vec, +} + +// Represents a unique identifier for a vertex in the IS-IS topology graph. +// +// `VertexId` is designed to serve as a key in collections, such as `BTreeMap`, +// that store vertices for the SPT and the tentative list. The `non_pseudonode` +// flag ensures that non-pseudonode vertices are given priority and processed +// first during the SPF algorithm. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct VertexId { + pub non_pseudonode: bool, + pub lan_id: LanId, +} + +// Represents a next-hop used to reach a vertex in the IS-IS topology graph. +// +// During the SPF computation, protocol-specific addresses (IPv4 and/or IPv6) +// are resolved and stored in this structure. This information is later used +// during route computation. +#[derive(Clone, Debug)] +#[derive(new)] +pub struct VertexNexthop { + pub system_id: SystemId, + pub iface_idx: InterfaceIndex, + pub ipv4: Option, + pub ipv6: Option, +} + +// Represents an IS reachability entry attached to a vertex. +#[derive(Debug, Eq, PartialEq)] +#[derive(new)] +pub struct VertexEdge { + pub id: VertexId, + pub cost: u32, +} + +// Represents an IP reachability entry attached to a vertex. +#[derive(Clone, Debug)] +#[derive(new)] +pub struct VertexNetwork { + pub prefix: IpNetwork, + pub metric: u32, + pub external: bool, +} + +// Container containing scheduling and timing information of SPF computations. #[derive(Debug, Default)] pub struct SpfScheduler { pub last_event_rcvd: Option, pub last_time: Option, + pub spf_type: SpfType, pub delay_state: fsm::State, pub delay_timer: Option, pub hold_down_timer: Option, @@ -39,12 +113,17 @@ pub struct SpfScheduler { pub schedule_time: Option, } -#[derive(Clone, Copy, Debug)] +// Type of SPF computation. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum SpfType { + // Full SPF computation. Full, + // "SPF computation of route reachability only. + #[default] RouteOnly, } +// SPF log entry. #[derive(Debug, new)] pub struct SpfLogEntry { pub id: u32, @@ -80,17 +159,35 @@ pub mod fsm { } } +// ===== impl VertexId ===== + +impl VertexId { + fn new(lan_id: LanId) -> VertexId { + VertexId { + non_pseudonode: !lan_id.is_pseudonode(), + lan_id, + } + } +} + // ===== global functions ===== +// Invokes an event in the SPF delay state machine. pub(crate) fn fsm( level: LevelNumber, event: fsm::Event, instance: &mut InstanceUpView<'_>, arenas: &mut InstanceArenas, ) -> Result<(), Error> { + // Begin a debug span for logging within the SPF context. + let span = debug_span!("spf", ?level); + let _span_guard = span.enter(); + + // Retrieve the SPF scheduling container for the current level. let spf_sched = instance.state.spf_sched.get_mut(level); - Debug::SpfDelayFsmEvent(level, spf_sched.delay_state, event).log(); + // Log the received event. + Debug::SpfDelayFsmEvent(spf_sched.delay_state, event).log(); // Update time of last SPF triggering event. spf_sched.last_event_rcvd = Some(Instant::now()); @@ -257,12 +354,8 @@ pub(crate) fn fsm( let spf_sched = instance.state.spf_sched.get_mut(level); if new_fsm_state != spf_sched.delay_state { // Effectively transition to the new FSM state. - Debug::SpfDelayFsmTransition( - level, - spf_sched.delay_state, - new_fsm_state, - ) - .log(); + Debug::SpfDelayFsmTransition(spf_sched.delay_state, new_fsm_state) + .log(); spf_sched.delay_state = new_fsm_state; } } @@ -276,9 +369,9 @@ pub(crate) fn fsm( fn compute_spf( level: LevelNumber, instance: &mut InstanceUpView<'_>, - _interfaces: &Interfaces, - _adjacencies: &Arena, - _lsp_entries: &Arena, + interfaces: &Interfaces, + adjacencies: &Arena, + lsp_entries: &Arena, ) { let spf_sched = instance.state.spf_sched.get_mut(level); @@ -292,7 +385,51 @@ fn compute_spf( // Get list of new or updated LSPs that triggered the SPF computation. let trigger_lsps = std::mem::take(&mut spf_sched.trigger_lsps); - // TODO: Run SPF. + // Compute shorted-path tree if necessary. + let spf_type = std::mem::take(&mut spf_sched.spf_type); + if spf_type == SpfType::Full { + let spt = + compute_spt(level, instance, interfaces, adjacencies, lsp_entries); + *instance.state.spt.get_mut(level) = spt; + } + + // Compute routing table. + let mut rib = compute_routes(level, instance, lsp_entries); + + // Update local routing table + match instance.config.level_type { + LevelType::L1 | LevelType::L2 => { + let old_rib = + std::mem::take(instance.state.rib_single.get_mut(level)); + + // Update the global RIB. + route::update_global_rib(&mut rib, old_rib, instance, interfaces); + + // Store the RIB specific to the current level. + *instance.state.rib_single.get_mut(level) = rib; + } + LevelType::All => { + let old_rib = std::mem::take(&mut instance.state.rib_multi); + + // Store the RIB specific to the current level. + *instance.state.rib_single.get_mut(level) = rib; + + // Build a merged RIB where L1 routes are preferred over L2 routes. + let rib_l1 = instance.state.rib_single.get(LevelNumber::L1); + let rib_l2 = instance.state.rib_single.get(LevelNumber::L2); + let mut rib = rib_l2 + .iter() + .chain(rib_l1.iter()) + .map(|(prefix, route)| (*prefix, route.clone())) + .collect(); + + // Update the global RIB. + route::update_global_rib(&mut rib, old_rib, instance, interfaces); + + // Store the merged RIB. + instance.state.rib_multi = rib; + } + } // Update statistics. instance.state.counters.get_mut(level).spf_runs += 1; @@ -300,13 +437,14 @@ fn compute_spf( // Update time of last SPF computation. let end_time = Instant::now(); + let spf_sched = instance.state.spf_sched.get_mut(level); spf_sched.last_time = Some(end_time); // Add entry to SPF log. log_spf_run( level, instance, - SpfType::Full, + spf_type, schedule_time, start_time, end_time, @@ -314,6 +452,405 @@ fn compute_spf( ); } +// Computes the shortest-path tree. +fn compute_spt( + level: LevelNumber, + instance: &InstanceUpView<'_>, + interfaces: &Interfaces, + adjacencies: &Arena, + lsp_entries: &Arena, +) -> BTreeMap { + let lsdb = instance.state.lsdb.get(level); + let metric_type = instance.config.metric_type.get(level); + let mut used_adjs = BTreeSet::new(); + + // Get root vertex. + let root_lan_id = LanId::from((instance.config.system_id.unwrap(), 0)); + let root_vid = VertexId::new(root_lan_id); + let root_v = Vertex::new(root_vid, 0, 0); + + // Initialize SPT and candidate list. + let mut spt = BTreeMap::new(); + let mut cand_list = BTreeMap::new(); + cand_list.insert((root_v.distance, root_v.id), root_v); + + // Main SPF loop. + 'spf_loop: while let Some(((_, vertex_id), vertex)) = cand_list.pop_first() + { + // Add vertex to SPT. + spt.insert(vertex.id, vertex); + let vertex = spt.get(&vertex_id).unwrap(); + + // Skip bad LSPs. + let Some(zeroth_lsp) = zeroth_lsp(vertex.id.lan_id, lsdb, lsp_entries) + else { + continue; + }; + + // If the overload bit is set, we skip the links from it. + if !zeroth_lsp.lsp_id.is_pseudonode() + && zeroth_lsp.flags.contains(LspFlags::OL) + { + continue; + } + + // In dual-stack single-topology networks, traffic blackholing can occur + // if any IS or link has IPv4 enabled but not IPv6, or vice versa. + // To minimize the likelihood of such issues, this check ensures that + // the IS supports all configured protocols. We can't check address + // family information from the links since that information isn't + // available in the LSPDB. + // + // NOTE: This check should be revisited and adapted once multi-topology + // support is implemented. + if !zeroth_lsp.lsp_id.is_pseudonode() { + let Some(protocols_supported) = + &zeroth_lsp.tlvs.protocols_supported + else { + Debug::SpfMissingProtocolsTlv(vertex).log(); + continue; + }; + for af in [AddressFamily::Ipv4, AddressFamily::Ipv6] { + if instance.config.is_af_enabled(af) + && !protocols_supported.contains(Nlpid::from(af)) + { + Debug::SpfUnsupportedProtocol(vertex, af).log(); + continue 'spf_loop; + } + } + } + + // Iterate over all links described by the vertex's LSPs. + for link in vertex_edges(&vertex.id, metric_type, lsdb, lsp_entries) { + // Check if the LSPs are mutually linked. + if !vertex_edges(&link.id, metric_type, lsdb, lsp_entries) + .any(|link| link.id == vertex.id) + { + continue; + } + + // Check if the link's vertex is already on the shortest-path tree. + if spt.contains_key(&link.id) { + continue; + } + + // Calculate distance to the link's vertex. + let distance = vertex.distance.saturating_add(link.cost); + + // Check maximum total metric value. + let max_path_metric = match metric_type { + MetricType::Wide | MetricType::Both => MAX_PATH_METRIC_WIDE, + MetricType::Standard => MAX_PATH_METRIC_STANDARD, + }; + if distance > max_path_metric { + Debug::SpfMaxPathMetric(vertex, &link, distance).log(); + continue; + } + + // Increment number of hops to the root. + let mut hops = vertex.hops; + if !link.id.lan_id.is_pseudonode() { + hops = hops.saturating_add(1); + } + + // Check if this vertex is already present on the candidate list. + if let Some((cand_key, cand_v)) = cand_list + .iter_mut() + .find(|(_, cand_v)| cand_v.id == link.id) + { + match distance.cmp(&cand_v.distance) { + Ordering::Less => { + // Remove vertex since its key has changed. It will be + // re-added with the correct key below. + let cand_key = *cand_key; + cand_list.remove(&cand_key); + } + Ordering::Equal => {} + Ordering::Greater => { + // Ignore higher cost path. + continue; + } + } + } + let cand_v = cand_list + .entry((distance, link.id)) + .or_insert_with(|| Vertex::new(link.id, distance, hops)); + + // Update vertex's nexthops. + if vertex.hops == 0 { + if !link.id.lan_id.is_pseudonode() + && let Some(nexthop) = compute_nexthop( + level, + vertex, + &link, + &mut used_adjs, + interfaces, + adjacencies, + ) + { + cand_v.nexthops.push(nexthop); + } + } else { + cand_v.nexthops.extend(vertex.nexthops.clone()); + }; + } + } + + spt +} + +// Computes routing table based on the SPT and IP prefix information extracted +// from the vertices. +fn compute_routes( + level: LevelNumber, + instance: &InstanceUpView<'_>, + lsp_entries: &Arena, +) -> BTreeMap { + let lsdb = instance.state.lsdb.get(level); + let metric_type = instance.config.metric_type.get(level); + + // Initialize routing table. + let mut rib = BTreeMap::new(); + + // Populate RIB. + let ipv4_enabled = instance.config.is_af_enabled(AddressFamily::Ipv4); + let ipv6_enabled = instance.config.is_af_enabled(AddressFamily::Ipv6); + for vertex in instance.state.spt.get(level).values() { + for network in vertex_networks( + &vertex.id, + metric_type, + ipv4_enabled, + ipv6_enabled, + lsdb, + lsp_entries, + ) { + let route = match rib.entry(network.prefix) { + btree_map::Entry::Vacant(v) => { + // If the route does not exist, create a new entry. + let route = Route::new(vertex, &network, level); + v.insert(route) + } + btree_map::Entry::Occupied(o) => { + let curr_route = o.into_mut(); + + let route_metric = vertex.distance + network.metric; + match route_metric.cmp(&curr_route.metric) { + Ordering::Less => { + // Replace route with a better one. + *curr_route = Route::new(vertex, &network, level); + } + Ordering::Equal => { + // Merge nexthops (anycast route). + curr_route.merge_nexthops(vertex, &network); + } + Ordering::Greater => { + // Ignore less preferred route. + continue; + } + } + + curr_route + } + }; + + // Honor configured maximum number of ECMP paths. + let max_paths = instance.config.max_paths; + if route.nexthops.len() > max_paths as usize { + route.nexthops = route + .nexthops + .iter() + .map(|(k, v)| (*k, *v)) + .take(max_paths as usize) + .collect(); + } + } + } + + rib +} + +// Computes the next-hop for reaching a vertex via the specified edge. +fn compute_nexthop( + level: LevelNumber, + vertex: &Vertex, + link: &VertexEdge, + used_adjs: &mut BTreeSet<[u8; 6]>, + interfaces: &Interfaces, + adjacencies: &Arena, +) -> Option { + // Check expected interface type. + let interface_type = if vertex.id.lan_id.is_pseudonode() { + InterfaceType::Broadcast + } else { + InterfaceType::PointToPoint + }; + + let (iface, adj) = interfaces + .iter() + .filter(|iface| iface.config.interface_type == interface_type) + .filter_map(|iface| { + let adj = match iface.config.interface_type { + InterfaceType::Broadcast => iface + .state + .lan_adjacencies + .get(level) + .get_by_system_id(adjacencies, &link.id.lan_id.system_id) + .map(|(_, adj)| adj) + .filter(|adj| adj.state == AdjacencyState::Up), + InterfaceType::PointToPoint => { + if iface.config.metric.get(level) != link.cost { + return None; + } + iface + .state + .p2p_adjacency + .as_ref() + .filter(|adj| adj.level_usage.intersects(level)) + .filter(|adj| adj.system_id == link.id.lan_id.system_id) + .filter(|adj| adj.state == AdjacencyState::Up) + } + }?; + Some((iface, adj)) + }) + // The same adjacency shouldn't be used more than once. + .find(|(_, adj)| used_adjs.insert(adj.snpa))?; + + Some(VertexNexthop { + system_id: adj.system_id, + iface_idx: iface.index, + ipv4: adj.ipv4_addrs.first().copied(), + ipv6: adj.ipv6_addrs.first().copied(), + }) +} + +// Iterate over all IS reachability entries attached to a vertex. +fn vertex_edges<'a>( + vertex_id: &VertexId, + metric_type: MetricType, + lsdb: &'a Lsdb, + lsp_entries: &'a Arena, +) -> impl Iterator + 'a { + // Iterate over all LSP fragments. + lsdb.iter_for_lan_id(lsp_entries, vertex_id.lan_id) + .map(|lse| &lse.data) + .filter(|lsp| lsp.seqno != 0) + .filter(|lsp| lsp.rem_lifetime != 0) + .flat_map(move |lsp| { + let mut iter: Box> = + Box::new(std::iter::empty()); + + if metric_type.is_standard_enabled() { + iter = Box::new(iter.chain(lsp.tlvs.is_reach().map(|reach| { + VertexEdge { + id: VertexId::new(reach.neighbor), + cost: reach.metric.into(), + } + }))); + } + if metric_type.is_wide_enabled() { + iter = Box::new(iter.chain(lsp.tlvs.ext_is_reach().map( + |reach| VertexEdge { + id: VertexId::new(reach.neighbor), + cost: reach.metric, + }, + ))); + } + + iter + }) +} + +// Iterate over all IP reachability entries attached to a vertex. +fn vertex_networks<'a>( + vertex_id: &VertexId, + metric_type: MetricType, + ipv4_enabled: bool, + ipv6_enabled: bool, + lsdb: &'a Lsdb, + lsp_entries: &'a Arena, +) -> impl Iterator + 'a { + // Iterate over all LSP fragments. + lsdb.iter_for_lan_id(lsp_entries, vertex_id.lan_id) + .map(|lse| &lse.data) + .filter(|lsp| lsp.seqno != 0) + .filter(|lsp| lsp.rem_lifetime != 0) + .flat_map(move |lsp| { + let mut iter: Box> = + Box::new(std::iter::empty()); + + // Iterate over IPv4 reachability entries. + if ipv4_enabled { + if metric_type.is_standard_enabled() { + let internal = + lsp.tlvs.ipv4_internal_reach().map(|reach| { + VertexNetwork { + prefix: reach.prefix.into(), + metric: reach.metric(), + external: false, + } + }); + // NOTE: RFC 1195 initially restricted the IP External + // Reachability Information TLV to L2 LSPs, but RFC 5302 + // later lifted this restriction. + let external = + lsp.tlvs.ipv4_external_reach().map(|reach| { + VertexNetwork { + prefix: reach.prefix.into(), + metric: reach.metric(), + external: true, + } + }); + + iter = Box::new(iter.chain(internal).chain(external)); + } + if metric_type.is_wide_enabled() { + iter = Box::new(iter.chain(lsp.tlvs.ext_ipv4_reach().map( + |reach| VertexNetwork { + prefix: reach.prefix.into(), + metric: reach.metric, + // For some reason, TLV 135 doesn't have a flag + // specifying whether the prefix has an external + // origin, unlike TLV 235 (the IPv6 equivalent). + // RFC 7794 specifies the Prefix Attributes + // Sub-TLV which contains the External Prefix + // Flag (X-flag). For now, let's just assume + // all prefixes announced using this TLV are + // internal. + external: false, + }, + ))); + } + } + + // Iterate over IPv6 reachability entries. + if ipv6_enabled { + iter = + Box::new(iter.chain(lsp.tlvs.ipv6_reach().map(|reach| { + VertexNetwork { + prefix: reach.prefix.into(), + metric: reach.metric, + external: reach.external, + } + }))); + } + + iter + }) +} + +// Retrieves the zeroth LSP for a given LAN ID. +fn zeroth_lsp<'a>( + lan_id: LanId, + lsdb: &'a Lsdb, + lsp_entries: &'a Arena, +) -> Option<&'a Lsp> { + let lspid = LspId::from((lan_id, 0)); + lsdb.get_by_lspid(lsp_entries, &lspid) + .map(|(_, lse)| &lse.data) + .filter(|lsp| lsp.seqno != 0) + .filter(|lsp| lsp.rem_lifetime != 0) +} + // Adds log entry for the SPF run. fn log_spf_run( level: LevelNumber,