use tinyvec::ArrayVec;
use crate::{
address::{addr::ScionAddr, socket_addr::ScionSocketAddr},
dataplane_path::{
model::DpPath,
standard::{
mac::{ForwardingKey, algo::mac_beta_step},
model::{HopField, InfoField, Segment, StandardPath},
types::{HopFieldFlags, HopFieldMac, InfoFieldFlags, exp_time_to_duration},
},
},
identifier::{asn::Asn, isd::Isd, isd_asn::IsdAsn},
packet::model::{ScionRawPacket, ScionScmpPacket, ScionUdpPacket},
path::{
ScionPath,
metadata::{InterfaceMetadata, PathMetadata, path_interface::PathInterface},
},
payload::scmp::model::ScmpMessage,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TestPathBuilder {
segments: Vec<TestPathBuilderSegment>,
src_address: ScionAddr,
dst_address: ScionAddr,
current_isd: u16,
current_asn: u32,
default_timestamp: u32,
default_hop_expiry: u8,
default_key: ForwardingKey,
}
impl TestPathBuilder {
pub fn new(src_address: ScionAddr, dst_address: ScionAddr) -> Self {
let current_isd = src_address.isd_asn().isd().0;
let current_asn = src_address.isd_asn().asn().0 as u32;
TestPathBuilder {
src_address,
dst_address,
current_isd,
current_asn,
default_timestamp: 0,
default_hop_expiry: 255,
segments: Vec::new(),
default_key: [0u8; 16],
}
}
#[inline]
pub fn using_info_timestamp(mut self, timestamp: u32) -> Self {
self.default_timestamp = timestamp;
self
}
#[inline]
pub fn with_hop_expiry(mut self, exp_time: u8) -> Self {
self.default_hop_expiry = exp_time;
self
}
#[inline]
pub fn using_forwarding_key(mut self, key: ForwardingKey) -> Self {
self.default_key = key;
self
}
#[inline]
pub fn with_isd(mut self, isd: u16) -> Self {
self.current_isd = isd;
self
}
#[inline]
pub fn with_asn(mut self, asn: u32) -> Self {
self.current_asn = asn;
self
}
#[inline]
pub fn down(mut self) -> Self {
self.add_segment(
true,
self.default_timestamp,
TestRoutingLinkType::LinkToChild,
);
self
}
#[inline]
pub fn core(mut self) -> Self {
self.add_segment(
true,
self.default_timestamp,
TestRoutingLinkType::LinkToCore,
);
self
}
#[inline]
pub fn up(mut self) -> Self {
self.add_segment(
false,
self.default_timestamp,
TestRoutingLinkType::LinkToParent,
);
self
}
#[inline]
pub fn add_hop(self, ingress_if: u16, egress_if: u16) -> Self {
self.add_hop_internal(ingress_if, false, egress_if, false, false)
}
#[inline]
pub fn add_hop_with_egress_down(self, ingress_if: u16, egress_if: u16) -> Self {
self.add_hop_internal(ingress_if, false, egress_if, false, true)
}
#[inline]
pub fn add_hop_with_alerts(
self,
ingress_if: u16,
ingress_alert: bool,
egress_if: u16,
egress_alert: bool,
) -> Self {
self.add_hop_internal(ingress_if, ingress_alert, egress_if, egress_alert, false)
}
fn add_hop_internal(
mut self,
ingress_if: u16,
ingress_alert: bool,
egress_if: u16,
egress_alert: bool,
egress_down: bool,
) -> Self {
let current_segment = self
.segments
.last_mut()
.expect("Path must have at least one segment");
let isd = self.current_isd;
let asn = self.current_asn;
if egress_if != 0 {
self.current_asn += 1;
}
let dst_isd = self.dst_address.isd_asn().isd();
if current_segment.uplink_type == TestRoutingLinkType::LinkToCore {
self.current_isd = dst_isd.0;
}
let cons_dir = current_segment
.info_field
.flags
.contains(InfoFieldFlags::CONS_DIR);
let (ingress_link_type, egress_link_type) = match (ingress_if, egress_if) {
(0, 0) => (None, None), (0, _) => (None, Some(current_segment.uplink_type)), (_, 0) => (Some(current_segment.uplink_type.reverse()), None),
(..) => {
(
Some(current_segment.uplink_type.reverse()),
Some(current_segment.uplink_type),
)
}
};
current_segment.hop_fields.push(TestPathBuilderHopField {
isd_asn: IsdAsn::new(Isd(isd), Asn(asn as u64)),
cons_dir,
ingress_link_type,
ingress_if,
egress_if,
egress_link_type,
egress_interface_down: egress_down,
ingress_router_alert: ingress_alert,
egress_router_alert: egress_alert,
exp_time: self.default_hop_expiry,
segment_change_next: false,
forwarding_key: self.default_key,
});
self
}
#[inline]
pub fn build(self, routing_timestamp: u32) -> TestPathContext {
self.build_with_path_modifier(routing_timestamp, |p| p)
}
pub fn build_with_path_modifier(
mut self,
routing_timestamp: u32,
path_modifier: impl FnOnce(StandardPath) -> StandardPath,
) -> TestPathContext {
let src_address = self.src_address;
let dst_address = self.dst_address;
let mut segment_lengths: [u8; 3] = [0; 3];
self.segments.iter().enumerate().for_each(|(i, segment)| {
segment_lengths[i] = segment.hop_fields.len() as u8;
});
let path = match self.segments.is_empty() {
true => DpPath::Empty,
false => {
if let Some(last_segment) = self.segments.iter_mut().last()
&& let Some(last_hop) = last_segment.hop_fields.iter_mut().last()
{
last_hop.isd_asn = dst_address.isd_asn();
}
let mut path_segments = ArrayVec::new();
for segment in &self.segments {
let mut previous_accumulator = segment.info_field.segment_id;
let mut accumulator = previous_accumulator;
let const_dir_iter: Box<
dyn DoubleEndedIterator<Item = &TestPathBuilderHopField>,
> = match segment.info_field.flags.contains(InfoFieldFlags::CONS_DIR) {
true => Box::new(segment.hop_fields.iter()),
false => Box::new(segment.hop_fields.iter().rev()),
};
let mut hops = const_dir_iter
.cloned()
.map(|hop_definition| {
let forwarding_key = hop_definition.forwarding_key;
let hop = hop_definition
.into_hop_field(accumulator, segment.info_field.timestamp);
previous_accumulator = accumulator;
accumulator = mac_beta_step(accumulator, *hop.mac);
(hop, forwarding_key)
})
.collect::<Vec<_>>();
let mut final_info = segment.info_field;
if !segment.info_field.flags.contains(InfoFieldFlags::CONS_DIR) {
hops.reverse();
final_info.segment_id = previous_accumulator;
};
path_segments.push(Segment {
info_field: final_info,
hop_fields: hops.into_iter().map(|(hop, _)| hop).collect(),
});
}
let path = StandardPath {
current_info_field: 0,
current_hop_field: 0,
segments: path_segments,
};
path_modifier(path).into()
}
};
let expiration = self
.segments
.iter()
.flat_map(|seg| {
seg.hop_fields.iter().map(|hop| {
seg.info_field.timestamp + exp_time_to_duration(hop.exp_time).as_secs() as u32
})
})
.min()
.unwrap_or(u32::MAX);
let mut interfaces = Vec::new();
for segment in &self.segments {
for hop in &segment.hop_fields {
let isd_asn = hop.isd_asn;
if hop.ingress_if != 0 {
interfaces.push(InterfaceMetadata::new_without_metadata(PathInterface {
isd_asn,
id: hop.ingress_if,
}));
}
if hop.egress_if != 0 {
interfaces.push(InterfaceMetadata::new_without_metadata(PathInterface {
isd_asn,
id: hop.egress_if,
}));
}
}
}
let path_meta = PathMetadata {
expiration: expiration as u64,
mtu: 1280,
interfaces: Some(interfaces),
epic_auth: None,
notes: None,
};
TestPathContext {
data_plane_path: path,
path_meta,
timestamp: routing_timestamp,
test_segments: self.segments,
dst_address,
src_address,
}
}
fn add_segment(
&mut self,
is_construction_dir: bool,
timestamp: u32,
uplink_type: TestRoutingLinkType,
) {
if self.segments.len() >= 3 {
panic!("Path can not have more than 3 segments");
}
let info_field = InfoField {
flags: match is_construction_dir {
true => InfoFieldFlags::CONS_DIR,
false => InfoFieldFlags::empty(),
},
segment_id: 0,
timestamp,
};
if let Some(last) = self.segments.iter_mut().last() {
last.hop_fields
.iter_mut()
.last()
.expect("Last segment must have at least one hop")
.segment_change_next = true;
}
self.segments.push(TestPathBuilderSegment {
hop_fields: Vec::new(),
info_field,
uplink_type,
});
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TestPathContext {
pub data_plane_path: DpPath,
pub path_meta: PathMetadata,
pub timestamp: u32,
pub test_segments: Vec<TestPathBuilderSegment>,
pub src_address: ScionAddr,
pub dst_address: ScionAddr,
}
impl TestPathContext {
pub fn scion_packet_raw(&self, payload: &[u8]) -> ScionRawPacket {
ScionRawPacket::new(
self.src_address,
self.dst_address,
self.data_plane_path.clone(),
crate::payload::ProtocolNumber::Other(0),
payload.to_owned(),
)
}
pub fn scion_packet_udp(&self, payload: &[u8], src_port: u16, dst_port: u16) -> ScionUdpPacket {
ScionUdpPacket::new(
ScionSocketAddr::new(
self.src_address.isd_asn(),
self.src_address.host(),
src_port,
),
ScionSocketAddr::new(
self.dst_address.isd_asn(),
self.dst_address.host(),
dst_port,
),
self.data_plane_path.clone(),
payload.to_owned(),
)
}
pub fn scion_packet_scmp(&self, message: ScmpMessage) -> ScionScmpPacket {
ScionScmpPacket::new(
self.src_address,
self.dst_address,
self.data_plane_path.clone(),
message,
)
}
pub fn path(&self) -> ScionPath {
ScionPath::new(
self.src_address.isd_asn(),
self.dst_address.isd_asn(),
self.data_plane_path
.try_encode_to_owned_view()
.expect("Failed to encode path"),
Some(self.path_meta.clone()),
None,
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TestPathBuilderHopField {
pub isd_asn: IsdAsn,
pub cons_dir: bool,
pub ingress_if: u16,
pub ingress_link_type: Option<TestRoutingLinkType>,
pub ingress_router_alert: bool,
pub egress_if: u16,
pub egress_link_type: Option<TestRoutingLinkType>,
pub egress_router_alert: bool,
pub egress_interface_down: bool,
pub exp_time: u8,
pub forwarding_key: ForwardingKey,
pub segment_change_next: bool,
}
impl TestPathBuilderHopField {
fn into_hop_field(self, mac_beta: u16, timestamp: u32) -> HopField {
let (cons_ingress, cons_egress) = match self.cons_dir {
true => (self.ingress_if, self.egress_if),
false => (self.egress_if, self.ingress_if),
};
let (ingress_router_alert, egress_router_alert) = match self.cons_dir {
true => (self.ingress_router_alert, self.egress_router_alert),
false => (self.egress_router_alert, self.ingress_router_alert),
};
let mut flags = HopFieldFlags::empty();
flags.set(
HopFieldFlags::CONS_INGRESS_ROUTER_ALERT,
ingress_router_alert,
);
flags.set(HopFieldFlags::CONS_EGRESS_ROUTER_ALERT, egress_router_alert);
HopField {
cons_ingress,
cons_egress,
mac: HopFieldMac::zero(),
expiration_units: self.exp_time,
flags,
}
.with_calculated_mac(mac_beta, timestamp, &self.forwarding_key)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TestPathBuilderSegment {
pub info_field: InfoField,
pub hop_fields: Vec<TestPathBuilderHopField>,
pub uplink_type: TestRoutingLinkType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TestRoutingLinkType {
LinkToCore,
LinkToParent,
LinkToChild,
LinkToPeer,
}
impl TestRoutingLinkType {
#[inline]
pub const fn reverse(&self) -> Self {
match self {
TestRoutingLinkType::LinkToCore => TestRoutingLinkType::LinkToCore,
TestRoutingLinkType::LinkToParent => TestRoutingLinkType::LinkToChild,
TestRoutingLinkType::LinkToChild => TestRoutingLinkType::LinkToParent,
TestRoutingLinkType::LinkToPeer => TestRoutingLinkType::LinkToPeer,
}
}
}
#[cfg(test)]
mod test {
use std::net::IpAddr;
use crate::{
address::addr::ScionAddr,
identifier::{asn::Asn, isd::Isd, isd_asn::IsdAsn},
path::metadata::path_interface::PathInterface,
};
#[test]
fn should_generate_correct_interfaces() {
let ctx = super::TestPathBuilder::new(
ScionAddr::new(
IsdAsn::new(Isd(1), Asn(2)),
IpAddr::from([192, 168, 0, 1]).into(),
),
ScionAddr::new(
IsdAsn::new(Isd(2), Asn(2)),
IpAddr::from([10, 0, 0, 1]).into(),
),
)
.using_info_timestamp(1000)
.up()
.add_hop(0, 1) .with_asn(8)
.add_hop(2, 3) .with_asn(4)
.add_hop(4, 0) .core()
.add_hop(0, 5) .add_hop(6, 7) .add_hop(8, 0) .build(2000);
let expected_interfaces = vec![
(IsdAsn::new(Isd(1), Asn(2)), 1),
(IsdAsn::new(Isd(1), Asn(8)), 2),
(IsdAsn::new(Isd(1), Asn(8)), 3),
(IsdAsn::new(Isd(1), Asn(4)), 4),
(IsdAsn::new(Isd(1), Asn(4)), 5),
(IsdAsn::new(Isd(2), Asn(5)), 6),
(IsdAsn::new(Isd(2), Asn(5)), 7),
(IsdAsn::new(Isd(2), Asn(2)), 8),
]
.into_iter()
.map(|(isd_asn, id)| PathInterface { isd_asn, id })
.collect::<Vec<_>>();
let interfaces = ctx
.path_meta
.interfaces
.unwrap()
.iter()
.map(|interfaces| interfaces.interface)
.collect::<Vec<_>>();
assert_eq!(expected_interfaces, interfaces);
}
}