use anyhow::Context;
use scion_proto::{address::IsdAsn, packet::ScionPacketRaw};
use crate::network::scion::{
crypto::ForwardingKey,
routing::{
AsRoutingAction, AsRoutingInterfaceState, AsRoutingLinkType, LocalAsRoutingAction,
RoutingLogic, ScionNetworkTime,
},
topology::{ScionLinkType, ScionTopology},
};
pub struct ScionNetworkSim;
impl ScionNetworkSim {
pub fn simulate_traversal<RoutingImpl: RoutingLogic>(
topology: &ScionTopology,
scion_packet: &mut ScionPacketRaw,
now: ScionNetworkTime,
ingress_asn: IsdAsn,
) -> anyhow::Result<ScionNetworkSimOutput> {
let iter =
ScionNetworkSimIter::<RoutingImpl>::new(topology, scion_packet, now, ingress_asn)?;
let iter_result = iter
.last()
.context("traversal of topology returned none")??;
let local_action = match iter_result.action {
AsRoutingAction::Local(action) => action,
_ => {
return Err(anyhow::anyhow!(
"topology iteration should return a local action, but got: {:?}",
iter_result.action
));
}
};
Ok(ScionNetworkSimOutput {
at_as: iter_result.at_as,
at_ingress_interface: iter_result.at_ingress_interface,
action: local_action,
})
}
pub fn iter<'input, AsRoutingImpl: RoutingLogic>(
topology: &'input ScionTopology,
scion_packet: &'input mut ScionPacketRaw,
now: ScionNetworkTime,
ingress_asn: IsdAsn,
) -> anyhow::Result<ScionNetworkSimIter<'input, AsRoutingImpl>> {
ScionNetworkSimIter::new(topology, scion_packet, now, ingress_asn)
}
}
pub struct ScionNetworkSimIter<'input, AsRoutingImpl: RoutingLogic> {
topology: &'input ScionTopology,
scion_packet: &'input mut ScionPacketRaw,
now: ScionNetworkTime,
current_as: IsdAsn,
current_ingress_interface_id: u16,
current_forwarding_key: ForwardingKey,
finished: bool,
_phantom: std::marker::PhantomData<AsRoutingImpl>,
}
impl<'input, AsRoutingImpl: RoutingLogic> ScionNetworkSimIter<'input, AsRoutingImpl> {
fn new(
topology: &'input ScionTopology,
scion_packet: &'input mut ScionPacketRaw,
now: ScionNetworkTime,
ingress_as: IsdAsn,
) -> anyhow::Result<Self> {
let current_forwarding_key = topology
.as_map
.get(&ingress_as)
.with_context(|| format!("AS {ingress_as} does not exist in the topology"))?
.forwarding_key
.into();
Ok(Self {
topology,
scion_packet,
now,
current_as: ingress_as,
current_ingress_interface_id: 0,
current_forwarding_key,
finished: false,
_phantom: std::marker::PhantomData,
})
}
pub fn get_processing_as(&self) -> IsdAsn {
self.current_as
}
pub fn get_processing_interface_id(&self) -> u16 {
self.current_ingress_interface_id
}
fn next_step(&mut self) -> anyhow::Result<Option<ScionNetworkSimIterOutput>> {
if self.finished {
return Ok(None);
}
let processing_as = self.current_as;
let processing_ingress_interface_id = self.current_ingress_interface_id;
let processing_result = AsRoutingImpl::route(
processing_as,
self.scion_packet,
self.current_ingress_interface_id,
self.now,
&self.current_forwarding_key,
|if_id| {
let link = self.topology.get_scion_link(&processing_as, if_id)?;
let link_type = link.get_link_type(&processing_as)?;
let link_type = match link_type {
ScionLinkType::Core => AsRoutingLinkType::LinkToCore,
ScionLinkType::Child => AsRoutingLinkType::LinkToParent,
ScionLinkType::Parent => AsRoutingLinkType::LinkToChild,
ScionLinkType::Peer => AsRoutingLinkType::LinkToPeer,
};
Some(AsRoutingInterfaceState {
link_type,
is_up: link.is_up,
})
},
);
let processing_result: AsRoutingAction = processing_result.into();
if let AsRoutingAction::ForwardNextHop { link_interface_id } = processing_result {
let uplink = self
.topology
.get_scion_link(&self.current_as, link_interface_id)
.with_context(|| {
format!(
"no link for {}#{} to AS does not exist in the topology",
self.current_as, link_interface_id
)
})?;
let link_partner = uplink.get_peer(&self.current_as).with_context(|| {
format!("link {uplink:?} does not contain AS {}", self.current_as)
})?;
self.current_as = link_partner.isd_as;
self.current_ingress_interface_id = link_partner.if_id;
self.current_forwarding_key = self
.topology
.as_map
.get(&self.current_as)
.with_context(|| {
format!(
"AS {} does not exist in the topology even though a link to it exists",
self.current_as
)
})?
.forwarding_key
.into();
} else {
self.finished = true;
}
Ok(Some(ScionNetworkSimIterOutput {
at_as: processing_as,
at_ingress_interface: processing_ingress_interface_id,
action: processing_result,
finished: self.finished,
}))
}
}
impl<'input, AsRoutingImpl: RoutingLogic> Iterator for ScionNetworkSimIter<'input, AsRoutingImpl> {
type Item = anyhow::Result<ScionNetworkSimIterOutput>;
fn next(&mut self) -> Option<Self::Item> {
self.next_step().transpose()
}
}
pub struct ScionNetworkSimIterOutput {
pub at_as: IsdAsn,
pub at_ingress_interface: u16,
pub action: AsRoutingAction,
pub finished: bool,
}
#[derive(Debug)]
pub struct ScionNetworkSimOutput {
pub at_as: IsdAsn,
pub at_ingress_interface: u16,
pub action: LocalAsRoutingAction,
}
#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
use bytes::{Bytes, BytesMut};
use helper::*;
use scion_proto::{
address::{ScionAddr, ScionAddrV4},
packet::{ByEndpoint, FlowId, ScionPacketRaw},
path::{DataPlanePath, EncodedStandardPath},
scmp::ScmpErrorMessage,
};
use super::*;
use crate::network::scion::topology::ScionAs;
#[test_log::test]
fn should_successfully_route_on_existing_path() {
let mut topology = ScionTopology::new();
topology
.add_as(ScionAs::new("1-1".parse().unwrap()))
.unwrap()
.add_as(ScionAs::new("1-2".parse().unwrap()))
.unwrap()
.add_as(ScionAs::new("1-3".parse().unwrap()))
.unwrap()
.add_as(ScionAs::new("1-4".parse().unwrap()))
.unwrap();
topology
.add_link("1-1#1 up_to 1-2#2".parse().unwrap())
.unwrap()
.add_link("1-2#3 up_to 1-3#4".parse().unwrap())
.unwrap()
.add_link("1-3#5 up_to 1-4#6".parse().unwrap())
.unwrap();
let src_addr = ScionAddr::V4(ScionAddrV4::new(
"1-1".parse().unwrap(),
Ipv4Addr::new(1, 1, 1, 1),
));
let dst_addr = ScionAddr::V4(ScionAddrV4::new(
"1-4".parse().unwrap(),
Ipv4Addr::new(2, 2, 2, 2),
));
let mut packet = raw_scion_packet(src_addr, dst_addr, &Bytes::from_static(b"Test Payload"));
let result = ScionNetworkSim::simulate_traversal::<MockScionPacketProcessor>(
&topology,
&mut packet,
ScionNetworkTime::from_timestamp_secs(0),
src_addr.isd_asn(),
)
.expect("Should not fail to route");
match result.action {
LocalAsRoutingAction::ForwardLocal { target_address } => {
assert_eq!(target_address, dst_addr, "Target address mismatch");
}
_ => {
panic!(
"Expected a local forwarding decision, but got: {:?}",
result.action
)
}
}
assert_eq!(result.at_as, dst_addr.isd_asn(), "Final ISD-ASN mismatch");
assert_eq!(
result.at_ingress_interface, 6,
"Final ingress interface ID mismatch"
);
}
#[test_log::test]
fn should_fail_to_route_if_path_is_broken() {
let mut topology = ScionTopology::new();
let failing_as = "1-2".parse().unwrap();
topology
.add_as(ScionAs::new("1-1".parse().unwrap()))
.unwrap()
.add_as(ScionAs::new(failing_as))
.unwrap()
.add_as(ScionAs::new("1-3".parse().unwrap()))
.unwrap()
.add_as(ScionAs::new("1-4".parse().unwrap()))
.unwrap();
topology
.add_link("1-1#1 up_to 1-2#2".parse().unwrap())
.unwrap()
.add_link("1-3#5 up_to 1-4#6".parse().unwrap())
.unwrap();
let src_addr = ScionAddr::V4(ScionAddrV4::new(
"1-1".parse().unwrap(),
Ipv4Addr::new(1, 1, 1, 1),
));
let dst_addr = ScionAddr::V4(ScionAddrV4::new(
"1-4".parse().unwrap(),
Ipv4Addr::new(2, 2, 2, 2),
));
let mut packet = raw_scion_packet(src_addr, dst_addr, &Bytes::from_static(b"Test Payload"));
let result = ScionNetworkSim::simulate_traversal::<MockScionPacketProcessor>(
&topology,
&mut packet,
ScionNetworkTime::from_timestamp_secs(0),
src_addr.isd_asn(),
)
.expect("Should not fail to simulate");
assert!(result.at_as == failing_as, "Final ISD-ASN mismatch");
assert!(
result.at_ingress_interface == 2,
"Final ingress interface ID mismatch"
);
match result.action {
LocalAsRoutingAction::SendSCMPErrorResponse(err) => {
assert!(
matches!(err, ScmpErrorMessage::ParameterProblem(_)),
"Expected a ParameterProblem SCMP error"
);
}
_ => panic!("Expected a SCMP error response due to broken path"),
}
}
#[test_log::test]
fn should_iterate_as_expected() {
let mut topology = ScionTopology::new();
let as1 = ScionAs::new("1-1".parse().unwrap());
let as2 = ScionAs::new("1-2".parse().unwrap());
let as3 = ScionAs::new("1-3".parse().unwrap());
let as4 = ScionAs::new("1-4".parse().unwrap());
topology
.add_as(as1)
.unwrap()
.add_as(as2)
.unwrap()
.add_as(as3)
.unwrap()
.add_as(as4)
.unwrap();
topology
.add_link("1-1#1 up_to 1-2#2".parse().unwrap())
.unwrap()
.add_link("1-2#3 up_to 1-3#4".parse().unwrap())
.unwrap()
.add_link("1-3#5 up_to 1-4#6".parse().unwrap())
.unwrap();
let src_addr = ScionAddr::V4(ScionAddrV4::new(as1.isd_as, Ipv4Addr::new(1, 1, 1, 1)));
let dst_addr = ScionAddr::V4(ScionAddrV4::new(as4.isd_as, Ipv4Addr::new(2, 2, 2, 2)));
let mut packet = raw_scion_packet(src_addr, dst_addr, &Bytes::from_static(b"Test Payload"));
let mut iter = ScionNetworkSim::iter::<MockScionPacketProcessor>(
&topology,
&mut packet,
ScionNetworkTime::from_timestamp_secs(0),
src_addr.isd_asn(),
)
.expect("Should not fail to route");
check_step(
&mut iter,
0,
src_addr.isd_asn(),
AsRoutingAction::ForwardNextHop {
link_interface_id: 1,
},
false,
);
check_step(
&mut iter,
2,
as2.isd_as,
AsRoutingAction::ForwardNextHop {
link_interface_id: 3,
},
false,
);
check_step(
&mut iter,
4,
as3.isd_as,
AsRoutingAction::ForwardNextHop {
link_interface_id: 5,
},
false,
);
check_step(
&mut iter,
6,
as4.isd_as,
AsRoutingAction::Local(LocalAsRoutingAction::ForwardLocal {
target_address: dst_addr,
}),
true,
);
fn check_step(
iter: &mut ScionNetworkSimIter<MockScionPacketProcessor>,
expected_ingress_interface: u16,
expected_isd_asn: IsdAsn,
expected_action: AsRoutingAction,
expected_finished: bool,
) {
let res = iter.next().expect("Step").expect("No error");
assert_eq!(res.at_ingress_interface, expected_ingress_interface);
assert_eq!(res.at_as, expected_isd_asn);
assert_eq!(res.action, expected_action);
assert_eq!(res.finished, expected_finished);
}
assert!(
iter.next().is_none(),
"There should be no more steps after the last one"
);
}
mod helper {
use bytes::BufMut;
use scion_proto::{
scmp::{ScmpErrorMessage, ScmpParameterProblem},
wire_encoding::{WireDecode, WireEncodeVec},
};
use super::*;
pub struct MockScionPacketProcessor;
impl RoutingLogic for MockScionPacketProcessor {
fn route(
_local_as: IsdAsn,
scion_packet: &mut ScionPacketRaw,
ingress_interface_id: u16,
_now: ScionNetworkTime,
_as_forwarding_key: &ForwardingKey,
interface_link_type_lookup: impl Fn(u16) -> Option<AsRoutingInterfaceState>,
) -> Result<AsRoutingAction, ScmpErrorMessage> {
if ingress_interface_id == 6 {
return Ok(AsRoutingAction::Local(LocalAsRoutingAction::ForwardLocal {
target_address: scion_packet.headers.address.destination().unwrap(),
}));
}
interface_link_type_lookup(ingress_interface_id + 1) .ok_or_else(|| {
tracing::warn!(
"No link type found for interface ID {}",
ingress_interface_id
);
ScmpErrorMessage::ParameterProblem(
ScmpParameterProblem::new(
scion_proto::scmp::ParameterProblemCode::UnknownHopFieldConsEgressInterface , 0
, scion_packet.encode_to_bytes_vec().concat().into())
)
})?;
Ok(AsRoutingAction::ForwardNextHop {
link_interface_id: ingress_interface_id + 1,
})
}
}
pub fn raw_scion_packet(
source_addr: ScionAddr,
dest_addr: ScionAddr,
payload: &Bytes,
) -> ScionPacketRaw {
let endpoints = ByEndpoint {
source: source_addr,
destination: dest_addr,
};
let mut path_raw = BytesMut::with_capacity(36);
path_raw.put_u32(0x0000_2000);
path_raw.put_slice(&[0_u8; 32]);
let dp_path = DataPlanePath::Standard(
EncodedStandardPath::decode(&mut path_raw.freeze()).unwrap(),
);
ScionPacketRaw::new(
endpoints,
dp_path,
payload.clone(),
0,
FlowId::new(0).unwrap(),
)
.unwrap()
}
}
}