use std::net::Ipv4Addr;
use anyhow::{Context, bail};
use scion_proto::{
address::{IsdAsn, ScionAddr, ScionAddrV4},
packet::{
ByEndpoint, ScionPacketRaw, ScionPacketScmp, classify_scion_packet,
layout::ScionPacketOffset,
},
path::DataPlanePath,
scmp::{
DestinationUnreachableCode, ParameterProblemCode, ScmpDestinationUnreachable,
ScmpEchoReply, ScmpErrorMessage, ScmpMessage, ScmpMessageBase, ScmpParameterProblem,
ScmpTracerouteReply,
},
wire_encoding::WireEncodeVec,
};
use tracing::info_span;
use crate::network::{
local::receiver_registry::NetworkReceiverRegistry, scion::routing::LocalAsRoutingAction,
};
pub struct LocalNetworkSimulation<'input> {
local_as: IsdAsn,
local_if_id: u16,
receivers: &'input NetworkReceiverRegistry,
}
impl LocalNetworkSimulation<'_> {
pub fn new(
local_as: IsdAsn,
local_if_id: u16,
receivers: &NetworkReceiverRegistry,
) -> LocalNetworkSimulation {
LocalNetworkSimulation {
local_if_id,
local_as,
receivers,
}
}
pub fn dispatch(&self, packet: ScionPacketRaw) -> Result<(), ScmpErrorMessage> {
tracing::trace!("Dispatching packet into AS {}", self.local_as);
let Some(dest_ip) = packet.headers.address.destination() else {
tracing::warn!("No local address found in packet destination, cannot dispatch");
return Err(ScmpDestinationUnreachable::new(
DestinationUnreachableCode::AddressUnreachable,
packet.encode_to_bytes_vec().concat().into(),
)
.into());
};
if dest_ip.isd_asn() != self.local_as {
tracing::warn!(
"Packet destination AS {} does not match local AS {}, cannot dispatch",
dest_ip.isd_asn(),
self.local_as
);
return Err(ScmpParameterProblem::new(
ParameterProblemCode::NonLocalDelivery,
ScionPacketOffset::address_header().dst_host_addr().bytes(),
packet.encode_to_bytes_vec().concat().into(),
)
.into());
}
self.receivers
.by_addr(dest_ip)
.ok_or_else(|| {
tracing::warn!("No dispatcher found for {dest_ip}");
ScmpDestinationUnreachable::new(
DestinationUnreachableCode::AddressUnreachable,
packet.encode_to_bytes_vec().concat().into(),
)
})?
.receive_packet(packet);
Ok(())
}
pub fn handle_local_routing_action(
&self,
action: LocalAsRoutingAction,
packet: ScionPacketRaw,
) -> anyhow::Result<Option<ScionPacketScmp>> {
let pkt_source_as = packet
.headers
.address
.source()
.map(|s| s.isd_asn())
.unwrap_or(IsdAsn(0));
let scmp_reply = match action {
LocalAsRoutingAction::ForwardLocal { target_address: _ } => {
match self.dispatch(packet.clone()) {
Ok(_) => None,
Err(scmp_error_message) => {
maybe_create_scmp_reply(self.local_as, scmp_error_message.into(), packet)
.context("error creating SCMP reply after dispatching failed with ")?
}
}
}
LocalAsRoutingAction::SendSCMPErrorResponse(scmp_error_message) => {
maybe_create_scmp_reply(self.local_as, scmp_error_message.into(), packet)?
}
LocalAsRoutingAction::IngressSCMPHandleRequest { interface_id } => {
debug_assert_eq!(
self.local_if_id, interface_id,
"This should always be the interface of the router"
);
self.handle_scmp(false, packet)
.context("error handling SCMP request")?
}
LocalAsRoutingAction::EgressSCMPHandleRequest { interface_id } => {
debug_assert_eq!(
self.local_if_id, interface_id,
"This should always be the interface of the router"
);
self.handle_scmp(true, packet)
.context("error handling SCMP request")?
}
};
let Some(scmp_reply) = scmp_reply else {
return Ok(None);
};
if pkt_source_as != self.local_as {
return Ok(Some(scmp_reply));
}
let _ = self.dispatch(scmp_reply.into()).inspect_err(|e| {
tracing::warn!("error dispatching SCMP back into AS: {}", e.to_string())
});
Ok(None) }
pub fn handle_scmp(
&self,
egress: bool,
packet: ScionPacketRaw,
) -> anyhow::Result<Option<ScionPacketScmp>> {
let _s = info_span!(
"loc-scmp",
local = %self.local_as,
iid = self.local_if_id,
egress = egress
)
.entered();
tracing::trace!("Handling SCMP");
let request = classify_scion_packet(packet)
.context("error classifying SCION packet for SCMP response")?
.try_into_scmp()
.map_err(|_| anyhow::anyhow!("packet was not a SCMP message"))?;
match &request.message {
ScmpMessage::EchoRequest(scmp_echo_request) => {
tracing::trace!("Handling SCMP EchoRequest");
maybe_create_scmp_reply(
self.local_as,
ScmpMessage::EchoReply(ScmpEchoReply::new(
scmp_echo_request.identifier,
scmp_echo_request.sequence_number,
scmp_echo_request.data.clone(),
)),
request.into(),
)
}
ScmpMessage::TracerouteRequest(scmp_traceroute_request) => {
tracing::trace!("Handling SCMP TracerouteRequest");
maybe_create_scmp_reply(
self.local_as,
ScmpMessage::TracerouteReply(ScmpTracerouteReply::new(
scmp_traceroute_request.identifier,
scmp_traceroute_request.sequence_number,
self.local_as,
self.local_if_id as u64,
)),
request.into(),
)
}
_ => {
tracing::warn!("Received unexpected SCMP message {:?}", request.message);
bail!("Unexpected SCMP message");
}
}
}
}
fn maybe_create_scmp_reply(
local_as: IsdAsn,
scmp: ScmpMessage,
respond_to: ScionPacketRaw,
) -> anyhow::Result<Option<ScionPacketScmp>> {
let classify = classify_scion_packet(respond_to.clone())
.context("error classifying SCION packet for SCMP response")?;
match classify {
scion_proto::packet::PacketClassification::ScmpWithDestination(_, pkt)
| scion_proto::packet::PacketClassification::ScmpWithoutDestination(pkt)
if pkt.message.is_error() =>
{
return Ok(None);
}
_ => {}
}
let packet_src = respond_to
.headers
.address
.source()
.context("packet has no source address")?;
let endhosts = ByEndpoint::<ScionAddr> {
source: ScionAddr::V4(ScionAddrV4::new(local_as, Ipv4Addr::new(0, 0, 0, 0))),
destination: packet_src,
};
let path = if packet_src.isd_asn() == local_as {
DataPlanePath::EmptyPath
} else {
let mut path = respond_to.headers.path.clone();
path.reverse().context("error reversing path from packet")?;
path
};
ScionPacketScmp::new(endhosts, path, scmp)
.context("error creating SCMP packet")
.map(Some)
}