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,