use std::{
borrow::Cow,
fmt::Display,
hash::{DefaultHasher, Hash, Hasher},
net,
time::{Duration, SystemTime},
};
use scion_proto::{
address::{HostAddr, IsdAsn},
packet::ScionPacketRaw,
path::{DataPlanePathFingerprint, Path},
scmp::{DestinationUnreachableCode, ScmpErrorMessage},
wire_encoding::WireDecode,
};
use crate::{
path::{manager::algo::exponential_decay, types::Score},
scionstack::ScionSocketSendError,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub 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 {
FullPath {
fingerprint: DataPlanePathFingerprint,
},
Interface {
isd_asn: IsdAsn,
ingress_filter: Option<u16>,
egress_filter: u16,
},
FirstHop {
isd_asn: IsdAsn,
egress_interface: u16,
},
LastHop {
isd_asn: IsdAsn,
ingress_interface: u16,
},
DestinationNetwork {
isd_asn: IsdAsn,
ingress_interface: u16,
dst_host: HostAddr,
},
}
impl IssueMarkerTarget {
pub fn matches_path(&self, path: &Path, fingerprint: &DataPlanePathFingerprint) -> bool {
self.matches_path_checked(path, fingerprint, |_, _| true)
}
pub fn matches_path_checked<F>(
&self,
path: &Path,
fingerprint: &DataPlanePathFingerprint,
might_include_check: F,
) -> bool
where
F: Fn(&IssueMarkerTarget, &Path) -> bool,
{
match self {
Self::FullPath {
fingerprint: target_fingerprint,
} => fingerprint == target_fingerprint,
Self::FirstHop {
isd_asn,
egress_interface,
} => {
path.first_hop_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_hop_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.source() == *isd_asn {
return match ingress_filter {
Some(_) => false,
None => {
interfaces
.first()
.is_some_and(|iface| &iface.id == egress_filter)
}
};
}
let mut iter = interfaces.iter();
while let Some(interface) = iter.nth(1) {
if interface.isd_asn != *isd_asn {
continue;
}
if let Some(ingress) = ingress_filter
&& interface.id != *ingress
{
return false;
}
return iter
.next()
.is_some_and(|egress| &egress.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 },
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, path: Option<&Path>) -> Option<IssueMarkerTarget> {
match self {
IssueKind::Scmp { error } => {
if path.is_none() {
debug_assert!(false, "Path must be provided on SCMP errors");
return None;
};
match error {
ScmpErrorMessage::DestinationUnreachable(scmp_destination_unreachable) => {
use scion_proto::scmp::DestinationUnreachableCode::*;
match scmp_destination_unreachable.code {
NoRouteToDestination
| AddressUnreachable
| BeyondScopeOfSourceAddress
| CommunicationAdministrativelyDenied
| SourceAddressFailedIngressEgressPolicy
| RejectRouteToDestination => {
let mut offending =
scmp_destination_unreachable.get_offending_packet();
let pkt = ScionPacketRaw::decode(&mut offending).ok()?;
let dst = pkt.headers.path().last_hop_ingress_interface()?;
let dst_host = pkt.headers.address.destination()?.host();
Some(IssueMarkerTarget::DestinationNetwork {
isd_asn: dst.isd_asn,
ingress_interface: dst.id,
dst_host,
})
}
Unassigned(_) | PortUnreachable | _ => None,
}
}
ScmpErrorMessage::ExternalInterfaceDown(msg) => {
Some(IssueMarkerTarget::Interface {
isd_asn: msg.isd_asn,
ingress_filter: None,
egress_filter: msg.interface_id as u16,
})
}
ScmpErrorMessage::InternalConnectivityDown(msg) => {
Some(IssueMarkerTarget::Interface {
isd_asn: msg.isd_asn,
ingress_filter: Some(msg.ingress_interface_id as u16),
egress_filter: msg.egress_interface_id as u16,
})
}
ScmpErrorMessage::Unknown(_) => None,
ScmpErrorMessage::PacketTooBig(_) => None,
ScmpErrorMessage::ParameterProblem(_) => None,
}
}
IssueKind::Icmp { .. } => None,
IssueKind::Socket { err } => {
if path.is_none() {
debug_assert!(false, "Path must be provided on Socket errors");
return None;
};
match err {
SendError::FirstHopUnreachable {
isd_asn,
interface_id,
..
} => {
Some(IssueMarkerTarget::FirstHop {
isd_asn: *isd_asn,
egress_interface: *interface_id,
})
}
}
}
}
}
pub 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 {
DestinationUnreachableCode::NoRouteToDestination
| DestinationUnreachableCode::AddressUnreachable => -0.8,
DestinationUnreachableCode::CommunicationAdministrativelyDenied => -0.9,
DestinationUnreachableCode::PortUnreachable => 0.0,
_ => -0.5,
}
}
ScmpErrorMessage::Unknown(_) => -0.2,
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,
address: Option<net::SocketAddr>,
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,
}
}
}