use std::{
collections::{HashSet, VecDeque},
hash::{DefaultHasher, Hash, Hasher},
time::{Duration, SystemTime},
};
use anyhow::Context;
use chrono::{DateTime, Utc};
use scion_proto::address::IsdAsn;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::network::scion::{
segment::{
model::LinkSegment,
registry::{LinkSegmentStore, SegmentRegistry},
},
topology::{ScionGlobalInterfaceId, ScionLinkType, ScionTopology},
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
pub struct InterfaceBeaconState {
pub interface: ScionGlobalInterfaceId,
pub is_core: bool,
pub hop_expiry_units: u8,
pub generate_forward_beacons: bool,
pub originator_ases: Option<HashSet<IsdAsn>>,
pub beacon_interval: Duration,
pub beacon_retry_interval: Duration,
pub next_send_time: DateTime<Utc>,
}
impl InterfaceBeaconState {
pub fn new(
is_core: bool,
interface: ScionGlobalInterfaceId,
generate_forward_beacons: bool,
originator_ases: Option<HashSet<IsdAsn>>,
) -> Self {
Self {
interface,
is_core,
hop_expiry_units: 255,
generate_forward_beacons,
originator_ases,
beacon_interval: Duration::from_secs(600),
beacon_retry_interval: Duration::from_secs(10),
next_send_time: Utc::now(),
}
}
pub fn with_beacon_interval(mut self, beacon_interval: Duration) -> Self {
self.beacon_interval = beacon_interval;
self
}
pub fn with_beacon_retry_interval(mut self, beacon_retry_interval: Duration) -> Self {
self.beacon_retry_interval = beacon_retry_interval;
self
}
}
pub enum InterfaceBeaconAction {
SendBeacons(Vec<scion_protobuf::control_plane::v1::BeaconRequest>),
Wait(SystemTime),
}
impl InterfaceBeaconState {
pub fn tick(
&self,
current_time: SystemTime,
segment_registry: &SegmentRegistry,
topology: &ScionTopology,
) -> anyhow::Result<InterfaceBeaconAction> {
if chrono::DateTime::<Utc>::from(current_time) >= self.next_send_time {
let mut beacons = if self.generate_forward_beacons {
tracing::debug!(
interface = %self.interface,
"Generating forwarding beacons for interface",
);
BeaconGen::generate_forwarding_beacons(
self.interface,
self.is_core,
self.originator_ases.as_ref(),
segment_registry,
topology,
current_time.into(),
self.hop_expiry_units,
)?
} else {
Vec::new()
};
if self.is_core {
tracing::debug!(
interface = %self.interface,
"Generating originating beacon for interface",
);
let originating_beacon = BeaconGen::generate_originating_beacons(
self.interface,
topology,
current_time.into(),
self.hop_expiry_units,
)?;
beacons.push(originating_beacon);
}
return Ok(InterfaceBeaconAction::SendBeacons(beacons));
}
Ok(InterfaceBeaconAction::Wait(self.next_send_time.into()))
}
pub fn mark_success(&mut self, current_time: SystemTime) {
self.next_send_time = chrono::DateTime::<Utc>::from(current_time) + self.beacon_interval;
}
pub fn mark_failure(&mut self, current_time: SystemTime) {
self.next_send_time =
chrono::DateTime::<Utc>::from(current_time) + self.beacon_retry_interval;
}
}
pub struct BeaconGen;
impl BeaconGen {
pub fn generate_forwarding_beacons(
egress_if: ScionGlobalInterfaceId,
is_core: bool,
originator_ases: Option<&HashSet<IsdAsn>>,
segments: &SegmentRegistry,
topology: &ScionTopology,
timestamp: DateTime<Utc>,
hop_expiry_units: u8,
) -> anyhow::Result<Vec<scion_protobuf::control_plane::v1::BeaconRequest>> {
let egress_link = topology
.scion_link(&egress_if.isd_as, egress_if.if_id)
.context("Given interface does not exist in topology")?
.get_directed_from(&egress_if.isd_as)
.expect(
"Topology is inconsistent, link does not have a direction from the expected AS",
);
let empty_segment_store = LinkSegmentStore::new(Default::default(), Default::default());
let our_as = egress_if.isd_as;
let forward_segments: Vec<_> = match is_core {
true => {
segments
.core_segments()
.segments_by_end_as(our_as)
.iter()
.filter(|seg| {
originator_ases.is_none_or(|oas| oas.contains(&seg.bucket.start_as))
})
.map(|seg| {
segments
.core_segments()
.segment(seg)
.expect("segment index and segment registry must be consistent")
})
.cloned()
.collect()
}
false => {
let our_relevant_interfaces: HashSet<_> = topology.iter_scion_links_by_as(&our_as).filter_map(|link| {
let directed = link.get_directed_to(&our_as).expect(
"Link in AS is not connected to the expected AS, topology is inconsistent with External AS state",
);
let our_interface = directed.to;
let relevant = match directed.link_type {
ScionLinkType::Parent => true, ScionLinkType::Core => false, ScionLinkType::Peer => false, ScionLinkType::Child => false,
};
match relevant {
true => Some(our_interface),
false => None,
}
}).collect();
let isd_segment_store = segments
.isd_segments(&our_as.isd())
.unwrap_or(&empty_segment_store);
isd_segment_store
.segments_by_end_as(our_as)
.iter()
.filter(|seg| {
originator_ases.is_none_or(|oas| oas.contains(&seg.bucket.start_as))
})
.map(|seg| {
isd_segment_store
.segment(seg)
.expect("segment index and segment registry must be consistent")
})
.filter(|seg| {
seg.links
.iter()
.last()
.map(|link| our_relevant_interfaces.contains(&link.to))
.unwrap_or(false)
})
.cloned()
.collect::<Vec<_>>()
}
};
let mut beacons = Vec::new();
for mut segment in forward_segments {
segment.links.push_back(egress_link.clone());
let mut hasher = DefaultHasher::new();
timestamp.hash(&mut hasher);
let segment_id = hasher.finish() as u16;
let path_segment = segment
.to_path_segment(topology, timestamp, segment_id, hop_expiry_units, true)
.context("Failed to convert segment to path segment for beacon generation")?;
let beacon_req = scion_protobuf::control_plane::v1::BeaconRequest {
segment: Some(path_segment.into()),
};
beacons.push(beacon_req);
}
tracing::debug!(
num_beacons = beacons.len(),
asn = %egress_if,
"Generated forwarding beacons for AS",
);
Ok(beacons)
}
pub fn generate_originating_beacons(
sending_as_interface: ScionGlobalInterfaceId,
topology: &ScionTopology,
timestamp: DateTime<Utc>,
hop_expiry_units: u8,
) -> anyhow::Result<scion_protobuf::control_plane::v1::BeaconRequest> {
let link = topology
.scion_link(&sending_as_interface.isd_as, sending_as_interface.if_id)
.context("Given interface does not exist in topology")?
.get_directed_from(&sending_as_interface.isd_as)
.expect(
"Topology is inconsistent, link does not have a direction from the expected AS",
);
let peer_as = link.to;
let link_segment = LinkSegment {
start_as: sending_as_interface.isd_as.into(),
end_as: peer_as.isd_as.into(),
links: VecDeque::from_iter([link]),
};
let mut hasher = DefaultHasher::new();
timestamp.hash(&mut hasher);
let segment_id = hasher.finish() as u16;
let path_segment = link_segment
.to_path_segment(topology, timestamp, segment_id, hop_expiry_units, true)
.context("Failed to convert segment to path segment for beacon generation")?;
let beacon_req = scion_protobuf::control_plane::v1::BeaconRequest {
segment: Some(path_segment.into()),
};
Ok(beacon_req)
}
}