use std::{
borrow::Cow,
fmt::Display,
hash::{DefaultHasher, Hash, Hasher},
net,
time::{Duration, SystemTime},
};
use sciparse::{
address::host_addr::ScionHostAddr,
core::view::View,
dataplane_path::view::ScionDpPathViewExt,
identifier::isd_asn::IsdAsn,
packet::view::ScionRawPacketView,
path::{ScionPath, fingerprint::data_plane::DpPathFingerprint},
payload::scmp::{model::ScmpErrorMessage, types::ScmpDestinationUnreachableCode},
};
use crate::{
path::{manager::algo::exponential_decay, types::Score},
stack::ScionSocketSendError,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct IssueMarker {
pub target: IssueMarkerTarget,
pub timestamp: SystemTime,
pub penalty: Score,
}
impl IssueMarker {
const SYSTEM_HALF_LIFE: Duration = Duration::from_secs(30);
pub fn decayed_penalty(&self, now: SystemTime) -> Score {
let elapsed = now
.duration_since(self.timestamp)
.unwrap_or(Duration::from_secs(0));
let decayed = exponential_decay(self.penalty.value(), elapsed, Self::SYSTEM_HALF_LIFE);
Score::new_clamped(decayed)
}
}
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)]
pub enum IssueMarkerTarget {
#[allow(dead_code)]
FullPath { fingerprint: DpPathFingerprint },
Interface {
isd_asn: IsdAsn,
ingress_filter: Option<u16>,
egress_filter: u16,
},
FirstHop {
isd_asn: IsdAsn,
egress_interface: u16,
},
#[allow(dead_code)]
LastHop {
isd_asn: IsdAsn,
ingress_interface: u16,
},
DestinationNetwork {
isd_asn: IsdAsn,
ingress_interface: u16,
dst_host: ScionHostAddr,
},
}
impl IssueMarkerTarget {
pub fn matches_path(&self, path: &ScionPath, fingerprint: &DpPathFingerprint) -> bool {
self.matches_path_checked(path, fingerprint, |_, _| true)
}
pub fn matches_path_checked<F>(
&self,
path: &ScionPath,
fingerprint: &DpPathFingerprint,
might_include_check: F,
) -> bool
where
F: Fn(&IssueMarkerTarget, &ScionPath) -> bool,
{
match self {
Self::FullPath {
fingerprint: target_fingerprint,
} => fingerprint == target_fingerprint,
Self::FirstHop {
isd_asn,
egress_interface,
} => {
path.first_egress_interface()
.is_some_and(|intf| intf.isd_asn == *isd_asn && intf.id == *egress_interface)
}
Self::DestinationNetwork {
isd_asn,
ingress_interface,
..
}
| Self::LastHop {
isd_asn,
ingress_interface,
} => {
path.last_ingress_interface()
.is_some_and(|intf| intf.isd_asn == *isd_asn && intf.id == *ingress_interface)
}
Self::Interface {
isd_asn,
egress_filter,
ingress_filter,
} => {
if !might_include_check(self, path) {
return false;
}
let interfaces = match path
.metadata()
.as_ref()
.and_then(|meta| meta.interfaces.as_ref())
{
Some(interfaces) => interfaces,
None => return false, };
if path.src_ia() == *isd_asn {
return match ingress_filter {
Some(_) => false,
None => {
interfaces
.first()
.is_some_and(|iface| &iface.interface.id == egress_filter)
}
};
}
let mut iter = interfaces.iter();
while let Some(interface) = iter.nth(1) {
if interface.interface.isd_asn != *isd_asn {
continue;
}
if let Some(ingress) = ingress_filter
&& interface.interface.id != *ingress
{
return false;
}
return iter
.next()
.is_some_and(|egress| &egress.interface.id == egress_filter);
}
false
}
}
}
pub fn applies_to_multiple_paths(&self) -> bool {
match self {
IssueMarkerTarget::Interface { .. }
| IssueMarkerTarget::FirstHop { .. }
| IssueMarkerTarget::LastHop { .. }
| IssueMarkerTarget::DestinationNetwork { .. } => true,
IssueMarkerTarget::FullPath { .. } => false,
}
}
pub fn applies_to_path(&self, src: IsdAsn, dst: IsdAsn) -> bool {
match self {
IssueMarkerTarget::FullPath { .. } | IssueMarkerTarget::Interface { .. } => true,
IssueMarkerTarget::FirstHop { isd_asn, .. } => src == *isd_asn,
IssueMarkerTarget::LastHop { isd_asn, .. }
| IssueMarkerTarget::DestinationNetwork { isd_asn, .. } => dst == *isd_asn,
}
}
}
#[derive(Debug, Clone)]
pub enum IssueKind {
Scmp { error: ScmpErrorMessage },
#[allow(dead_code)]
Icmp {},
Socket { err: SendError },
}
impl Display for IssueKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IssueKind::Scmp { error } => write!(f, "SCMP Error: {error:?}"),
IssueKind::Icmp { .. } => write!(f, "ICMP Error"),
IssueKind::Socket { err } => write!(f, "Socket Error: {err:?}"),
}
}
}
impl IssueKind {
pub fn dedup_id(&self, marker: &IssueMarkerTarget) -> u64 {
let mut hasher = DefaultHasher::new();
marker.hash(&mut hasher);
let error_str = format!("{self}");
error_str.hash(&mut hasher);
hasher.finish()
}
pub fn target_type(&self) -> Option<IssueMarkerTarget> {
match self {
IssueKind::Scmp { error } => {
match error {
ScmpErrorMessage::DestinationUnreachable(scmp_destination_unreachable) => {
use sciparse::payload::scmp::types::ScmpDestinationUnreachableCode::{
AddressUnreachable, BeyondScopeOfSourceAddress,
CommunicationAdministrativelyDenied, NoRouteToDestination,
PortUnreachable, RejectRouteToDestination,
SourceAddressFailedIngressEgressPolicy, Unassigned,
};
match scmp_destination_unreachable.code {
NoRouteToDestination
| AddressUnreachable
| BeyondScopeOfSourceAddress
| CommunicationAdministrativelyDenied
| SourceAddressFailedIngressEgressPolicy
| RejectRouteToDestination => {
let offending = scmp_destination_unreachable.get_offending_packet();
let (pkt, _rest) =
ScionRawPacketView::try_from_slice(offending).ok()?;
let dst = pkt.header().path().last_ingress_interface()?;
let dst_host =
pkt.header().dst_host_addr().ok()?.scion_host_addr().ok()?;
Some(IssueMarkerTarget::DestinationNetwork {
isd_asn: pkt.header().dst_ia(),
ingress_interface: dst,
dst_host,
})
}
Unassigned(_) | PortUnreachable => None,
}
}
ScmpErrorMessage::ExternalInterfaceDown(msg) => {
Some(IssueMarkerTarget::Interface {
isd_asn: msg.isd_asn,
ingress_filter: None,
egress_filter: msg.interface_id,
})
}
ScmpErrorMessage::InternalConnectivityDown(msg) => {
Some(IssueMarkerTarget::Interface {
isd_asn: msg.isd_asn,
ingress_filter: Some(msg.ingress_interface_id),
egress_filter: msg.egress_interface_id,
})
}
ScmpErrorMessage::PacketTooBig(_) => None,
ScmpErrorMessage::ParameterProblem(_) => None,
}
}
IssueKind::Icmp { .. } => None,
IssueKind::Socket { err } => {
match err {
SendError::FirstHopUnreachable {
isd_asn,
interface_id,
..
} => {
Some(IssueMarkerTarget::FirstHop {
isd_asn: *isd_asn,
egress_interface: *interface_id,
})
}
}
}
}
}
pub(crate) fn penalty(&self) -> Score {
let magnitude = match self {
IssueKind::Scmp { error } => {
match error {
ScmpErrorMessage::ExternalInterfaceDown(_)
| ScmpErrorMessage::InternalConnectivityDown(_) => -1.0,
ScmpErrorMessage::DestinationUnreachable(err) => {
match err.code {
ScmpDestinationUnreachableCode::NoRouteToDestination
| ScmpDestinationUnreachableCode::AddressUnreachable => -0.8,
ScmpDestinationUnreachableCode::CommunicationAdministrativelyDenied => {
-0.9
}
ScmpDestinationUnreachableCode::PortUnreachable => 0.0,
_ => -0.5,
}
}
ScmpErrorMessage::PacketTooBig(_) | ScmpErrorMessage::ParameterProblem(_) => {
0.0
}
}
}
IssueKind::Socket { err } => {
match err {
SendError::FirstHopUnreachable { .. } => -0.4,
}
}
IssueKind::Icmp { .. } => 0.0,
};
Score::new_clamped(magnitude)
}
}
#[derive(Debug, Clone)]
pub enum SendError {
FirstHopUnreachable {
isd_asn: IsdAsn,
interface_id: u16,
#[allow(dead_code)]
address: Option<net::SocketAddr>,
#[allow(dead_code)]
msg: Cow<'static, str>,
},
}
impl SendError {
pub fn from_socket_send_error(error: &ScionSocketSendError) -> Option<Self> {
match error {
ScionSocketSendError::UnderlayNextHopUnreachable {
isd_as,
interface_id,
address,
msg,
} => {
Some(Self::FirstHopUnreachable {
isd_asn: *isd_as,
interface_id: *interface_id,
address: *address,
msg: msg.clone().into(),
})
}
_ => None,
}
}
}