Skip to content

Commit

Permalink
isis: implement the SPF algorithm and route computation
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
rwestphal committed Dec 22, 2024
1 parent 7332bd4 commit b9adc68
Show file tree
Hide file tree
Showing 11 changed files with 954 additions and 48 deletions.
15 changes: 14 additions & 1 deletion holo-isis/src/collections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<LspEntry>,
lan_id: LanId,
) -> impl Iterator<Item = &'a LspEntry> + '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.
Expand Down
46 changes: 36 additions & 10 deletions holo-isis/src/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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)]
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -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")
}
}
}
Expand Down
14 changes: 12 additions & 2 deletions holo-isis/src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -77,6 +79,11 @@ pub struct InstanceState {
pub lsp_orig_pending: Option<LevelType>,
// SPF scheduler state.
pub spf_sched: Levels<SpfScheduler>,
// Shortest-path tree.
pub spt: Levels<BTreeMap<VertexId, Vertex>>,
// Routing table (per-level and L1/L2).
pub rib_single: Levels<BTreeMap<IpNetwork, Route>>,
pub rib_multi: BTreeMap<IpNetwork, Route>,
// Event counters.
pub counters: Levels<InstanceCounters>,
pub discontinuity_time: DateTime<Utc>,
Expand Down Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions holo-isis/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
17 changes: 15 additions & 2 deletions holo-isis/src/lsdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
49 changes: 34 additions & 15 deletions holo-isis/src/northbound/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Callbacks<Instance>> = Lazy::new(load_callbacks);
Expand All @@ -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),
Expand Down Expand Up @@ -531,29 +535,44 @@ fn load_callbacks() -> Callbacks<Instance> {
})
})
.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)
Expand Down
18 changes: 18 additions & 0 deletions holo-isis/src/packet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
}
}
}
37 changes: 36 additions & 1 deletion holo-isis/src/packet/tlv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -215,6 +215,17 @@ pub struct UnknownTlv {
pub value: Bytes,
}

// ===== impl Nlpid =====

impl From<AddressFamily> for Nlpid {
fn from(af: AddressFamily) -> Nlpid {
match af {
AddressFamily::Ipv4 => Nlpid::Ipv4,
AddressFamily::Ipv6 => Nlpid::Ipv6,
}
}
}

// ===== impl AreaAddressesTlv =====

impl AreaAddressesTlv {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit b9adc68

Please sign in to comment.