use std::fmt::{Debug, Display};
use crate::dataplane_path::standard::{
mac::{
ForwardingKey,
algo::{calculate_hop_mac, mac_beta_step},
},
types::{HopFieldFlags, HopFieldMac, InfoFieldFlags},
view::{HopFieldView, InfoFieldView, StandardPathView},
};
#[derive(Debug, thiserror::Error)]
pub enum AdvanceError {
#[error("hop out of bounds: {0}")]
HopOutOfBounds(u8),
#[error("info out of bounds: {0}")]
InfoOutOfBounds(u8),
#[error(
"current hop field index is in segment {expected}, but info index is at segment {actual}"
)]
InvalidSegmentIndex {
expected: usize,
actual: usize,
},
#[error("path is in invalid state for advance: {0}")]
InvalidPathState(&'static str),
}
pub trait AdvanceValidator {
type Error: Debug;
fn validate_hop(
&self,
hop_index: usize,
hop_field: &HopFieldView,
info_field: &InfoFieldView,
is_segment_start: bool,
is_segment_end: bool,
) -> Result<(), Self::Error>;
fn validate_segment_change(
&self,
hop_index: usize,
current_hop_field: &HopFieldView,
current_info_field: &InfoFieldView,
next_hop_field: &HopFieldView,
next_info_field: &InfoFieldView,
) -> Result<(), Self::Error>;
}
struct NoValidation;
impl AdvanceValidator for NoValidation {
type Error = std::convert::Infallible;
#[inline]
fn validate_hop(
&self,
_hop_index: usize,
_hop_field: &HopFieldView,
_info_field: &InfoFieldView,
_is_segment_start: bool,
_is_segment_end: bool,
) -> Result<(), Self::Error> {
Ok(())
}
#[inline]
fn validate_segment_change(
&self,
_hop_index: usize,
_current_hop_field: &HopFieldView,
_current_info_field: &InfoFieldView,
_next_hop_field: &HopFieldView,
_next_info_field: &InfoFieldView,
) -> Result<(), Self::Error> {
Ok(())
}
}
#[derive(Debug)]
pub struct IngressAdvanceOutput {
pub scmp_alert: bool,
pub ingress_interface: u16,
pub action: IngressAdvanceAction,
}
#[derive(Debug)]
pub enum IngressAdvanceAction {
ContinueEgress {
egress_if: u16,
},
ForwardLocal,
}
#[derive(Debug)]
pub enum IngressValidateResult<ValidationErrorType> {
Ok(IngressAdvanceOutput),
ValidationFailed(IngressAdvanceOutput, ValidationErrorType),
}
impl<E> IngressValidateResult<E> {
#[inline]
pub fn into_result(self) -> Result<IngressAdvanceOutput, (IngressAdvanceOutput, E)> {
match self {
IngressValidateResult::Ok(output) => Ok(output),
IngressValidateResult::ValidationFailed(output, err) => Err((output, err)),
}
}
}
impl StandardPathView {
#[inline]
pub fn advance_ingress(
&mut self,
from_internal_interface: bool,
) -> Result<IngressAdvanceOutput, AdvanceError> {
match self.advance_ingress_with_validator(NoValidation, from_internal_interface)? {
IngressValidateResult::Ok(ingress_advance_result) => Ok(ingress_advance_result),
IngressValidateResult::ValidationFailed(..) => {
unreachable!("NoValidation is Infallible")
}
}
}
#[inline]
pub fn advance_ingress_with_validator<ValidatorType, E>(
&mut self,
validator: ValidatorType,
from_internal_interface: bool,
) -> Result<IngressValidateResult<E>, AdvanceError>
where
ValidatorType: AdvanceValidator<Error = E>,
E: Debug,
{
let hop_field_count = self.hop_field_count();
let curr_hop_idx = self.curr_hop_field_idx() as usize;
let curr_info_idx = self.curr_info_field_idx() as usize;
let (seg_idx, start_of_segment, end_of_segment) = self
.calculate_segment_index(curr_hop_idx)
.ok_or(AdvanceError::HopOutOfBounds(curr_hop_idx as u8))?;
if start_of_segment && end_of_segment {
return Err(AdvanceError::InvalidPathState(
"Path contains a segment with a single hop",
));
}
if seg_idx != curr_info_idx {
return Err(AdvanceError::InvalidSegmentIndex {
expected: seg_idx,
actual: curr_info_idx,
});
}
let is_final_hop = curr_hop_idx + 1 >= hop_field_count as usize;
let mut curr_hop_copy = *self
.hop_field(curr_hop_idx)
.ok_or(AdvanceError::HopOutOfBounds(curr_hop_idx as u8))?;
let mut curr_info_copy = *self
.curr_info_field()
.ok_or(AdvanceError::InfoOutOfBounds(curr_info_idx as u8))?;
let curr_ingress_interface = curr_hop_copy.ingress_interface(&curr_info_copy);
let in_construction_dir = curr_info_copy.flags().contains(InfoFieldFlags::CONS_DIR);
if !from_internal_interface && !in_construction_dir {
let curr_segment_id = curr_info_copy.segment_id();
let hop_mac = curr_hop_copy.mac();
let new_segment_id = mac_beta_step(curr_segment_id, *hop_mac.as_bytes());
curr_info_copy.set_segment_id(new_segment_id);
}
let mut validation_err = validator
.validate_hop(
curr_hop_idx,
&curr_hop_copy,
&curr_info_copy,
start_of_segment,
end_of_segment,
)
.err();
let scmp_alert = curr_hop_copy
.flags()
.normalized_ingress_router_alert(in_construction_dir);
if !from_internal_interface && scmp_alert {
let mut flags = curr_hop_copy.flags();
match in_construction_dir {
true => flags.remove(HopFieldFlags::CONS_INGRESS_ROUTER_ALERT),
false => flags.remove(HopFieldFlags::CONS_EGRESS_ROUTER_ALERT),
};
curr_hop_copy.set_flags(flags);
}
let res = match (is_final_hop, end_of_segment) {
(true, true) => {
IngressAdvanceOutput {
scmp_alert,
ingress_interface: curr_ingress_interface,
action: IngressAdvanceAction::ForwardLocal,
}
}
(false, false) => {
IngressAdvanceOutput {
scmp_alert,
ingress_interface: curr_ingress_interface,
action: IngressAdvanceAction::ContinueEgress {
egress_if: curr_hop_copy.egress_interface(&curr_info_copy),
},
}
}
(false, true) => {
let next_hop_field = self
.hop_field(curr_hop_idx + 1)
.ok_or(AdvanceError::HopOutOfBounds(curr_hop_idx as u8 + 1))?;
let next_info_field = self
.info_field(seg_idx + 1)
.ok_or(AdvanceError::InfoOutOfBounds((seg_idx + 1) as u8))?;
validation_err = validation_err.or_else(|| {
validator
.validate_segment_change(
curr_hop_idx,
&curr_hop_copy,
&curr_info_copy,
next_hop_field,
next_info_field,
)
.err()
});
let egress_if = next_hop_field.egress_interface(next_info_field);
validation_err = validation_err.or_else(|| {
validator
.validate_hop(
curr_hop_idx + 1,
next_hop_field,
next_info_field,
true, false, )
.err()
});
self.set_curr_hop_field((curr_hop_idx + 1) as u8);
self.set_curr_info_field((seg_idx + 1) as u8);
IngressAdvanceOutput {
scmp_alert,
ingress_interface: curr_ingress_interface,
action: IngressAdvanceAction::ContinueEgress { egress_if },
}
}
_ => {
unreachable!(
"The only case where we can have a final hop is when we are also at a segment end, which is handled by the first match arm"
)
}
};
*self
.info_field_mut(curr_info_idx)
.expect("If we can get the current info field without mut, we can get it with mut") =
curr_info_copy;
*self
.hop_field_mut(curr_hop_idx)
.expect("If we can get the current hop field without mut, we can get it with mut") =
curr_hop_copy;
match validation_err {
Some(err) => Ok(IngressValidateResult::ValidationFailed(res, err)),
None => Ok(IngressValidateResult::Ok(res)),
}
}
}
#[derive(Debug)]
pub struct EgressAdvanceOutput {
pub scmp_alert: bool,
pub egress_interface: u16,
}
#[derive(Debug)]
pub enum EgressValidateResult<ValidationErrorType> {
Ok(EgressAdvanceOutput),
ValidationFailed(EgressAdvanceOutput, ValidationErrorType),
}
impl<E> EgressValidateResult<E> {
#[inline]
pub fn into_result(self) -> Result<EgressAdvanceOutput, (EgressAdvanceOutput, E)> {
match self {
EgressValidateResult::Ok(output) => Ok(output),
EgressValidateResult::ValidationFailed(output, err) => Err((output, err)),
}
}
}
impl StandardPathView {
#[inline]
pub fn advance_egress(&mut self) -> Result<EgressAdvanceOutput, AdvanceError> {
match self.advance_egress_with_validator(NoValidation)? {
EgressValidateResult::Ok(egress_advance_result) => Ok(egress_advance_result),
EgressValidateResult::ValidationFailed(..) => {
unreachable!("NoValidation is Infallible")
}
}
}
#[inline]
pub fn advance_egress_with_validator<ValidatorType, E>(
&mut self,
validator: ValidatorType,
) -> Result<EgressValidateResult<E>, AdvanceError>
where
ValidatorType: AdvanceValidator<Error = E>,
E: Debug + Display,
{
let hop_field_count = self.hop_field_count();
let curr_hop_idx = self.curr_hop_field_idx() as usize;
let curr_info_idx = self.curr_info_field_idx() as usize;
let (seg_idx, start_of_segment, end_of_segment) = self
.calculate_segment_index(curr_hop_idx)
.ok_or(AdvanceError::HopOutOfBounds(curr_hop_idx as u8))?;
if seg_idx != curr_info_idx {
return Err(AdvanceError::InvalidSegmentIndex {
expected: seg_idx,
actual: curr_info_idx,
});
}
let is_final_hop = curr_hop_idx + 1 >= hop_field_count as usize;
let mut curr_hop_copy = *self
.hop_field(curr_hop_idx)
.ok_or(AdvanceError::HopOutOfBounds(curr_hop_idx as u8))?;
let mut curr_info_copy = *self
.curr_info_field()
.ok_or(AdvanceError::InfoOutOfBounds(curr_info_idx as u8))?;
let in_construction_dir = curr_info_copy.flags().contains(InfoFieldFlags::CONS_DIR);
if is_final_hop {
return Err(AdvanceError::HopOutOfBounds(curr_hop_idx as u8 + 1));
}
if end_of_segment {
return Err(AdvanceError::InvalidPathState(
"Path is at segment end, which must have been handled at ingress",
));
}
if seg_idx != curr_info_idx {
return Err(AdvanceError::InvalidSegmentIndex {
expected: seg_idx,
actual: curr_info_idx,
});
}
let validation_error = validator
.validate_hop(
curr_hop_idx,
&curr_hop_copy,
&curr_info_copy,
start_of_segment,
end_of_segment,
)
.err();
if in_construction_dir {
let curr_segment_id = curr_info_copy.segment_id();
let hop_mac = curr_hop_copy.mac();
let new_segment_id = mac_beta_step(curr_segment_id, *hop_mac.as_bytes());
curr_info_copy.set_segment_id(new_segment_id);
}
let scmp_alert = curr_hop_copy
.flags()
.normalized_egress_router_alert(in_construction_dir);
if scmp_alert {
let mut flags = curr_hop_copy.flags();
match in_construction_dir {
true => flags.remove(HopFieldFlags::CONS_EGRESS_ROUTER_ALERT),
false => flags.remove(HopFieldFlags::CONS_INGRESS_ROUTER_ALERT),
};
curr_hop_copy.set_flags(flags);
}
*self
.info_field_mut(curr_info_idx)
.expect("If we can get the current info field without mut, we can get it with mut") =
curr_info_copy;
*self
.hop_field_mut(curr_hop_idx)
.expect("If we can get the current hop field without mut, we can get it with mut") =
curr_hop_copy;
self.set_curr_hop_field((curr_hop_idx + 1) as u8);
let out = EgressAdvanceOutput {
scmp_alert,
egress_interface: curr_hop_copy.egress_interface(&curr_info_copy),
};
match validation_error {
Some(err) => Ok(EgressValidateResult::ValidationFailed(out, err)),
None => Ok(EgressValidateResult::Ok(out)),
}
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone)]
#[error("invalid hop field MAC: expected {expected:?}, got {actual:?}")]
pub struct InvalidMacError {
expected: HopFieldMac,
actual: HopFieldMac,
}
#[derive(Clone)]
pub struct HopMacValidator {
pub key: ForwardingKey,
}
impl AdvanceValidator for HopMacValidator {
type Error = InvalidMacError;
#[inline]
fn validate_hop(
&self,
_hop_index: usize,
hop_field: &HopFieldView,
info_field: &InfoFieldView,
_is_segment_start: bool,
_is_segment_end: bool,
) -> Result<(), Self::Error> {
let mac = hop_field.mac();
let expected_mac = calculate_hop_mac(
info_field.segment_id(),
info_field.timestamp(),
hop_field.exp_time(),
hop_field.cons_ingress(),
hop_field.cons_egress(),
&self.key,
);
if mac.0 != expected_mac {
Err(InvalidMacError {
expected: expected_mac.into(),
actual: mac,
})
} else {
Ok(())
}
}
#[inline]
fn validate_segment_change(
&self,
_hop_index: usize,
_current_hop_field: &HopFieldView,
_current_info_field: &InfoFieldView,
_next_hop_field: &HopFieldView,
_next_info_field: &InfoFieldView,
) -> Result<(), Self::Error> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use proptest::{prelude::Arbitrary, proptest, test_runner::Config};
use crate::{
core::{encode::WireEncode, view::View},
dataplane_path::standard::{
mac::ForwardingKey,
model::{
HopField, StandardPath,
ptest::{ArbitraryForwardingKeyGenerator, ArbitraryPathContext},
},
routing::{HopMacValidator, IngressAdvanceAction},
view::StandardPathView,
},
};
struct StaticKeyGen;
impl StaticKeyGen {
pub const STATIC_KEY: ForwardingKey = [0u8; 16];
}
impl ArbitraryForwardingKeyGenerator for StaticKeyGen {
fn generate(
&self,
_field: &HopField,
_segment_index: usize,
_segment_hop_index: usize,
_segment_change: bool,
) -> ForwardingKey {
Self::STATIC_KEY
}
}
#[test]
fn should_succeed_advancing_any_path() {
proptest!(
Config::with_cases(500),
|(path in StandardPath::arbitrary_with(ArbitraryPathContext {
forwarding_key_generator: Some(Arc::new(StaticKeyGen)),
..Default::default()
}))| {
test_imp(path)?;
}
);
fn test_imp(path: StandardPath) -> Result<(), proptest::test_runner::TestCaseError> {
let mut view = path.try_encode_to_vec()?;
let (view, rest) = StandardPathView::try_from_mut_slice(view.as_mut_slice())?;
if !rest.is_empty() {
return Err(proptest::test_runner::TestCaseError::Fail(
"Encoded path has remaining bytes".into(),
));
}
advance_path(view, None)?;
view.try_reverse()
.expect("Reverse should succeed when we have advanced through the path");
advance_path(view, None)?;
Ok(())
}
}
#[test]
fn should_succeed_reversing_at_any_point() {
proptest!(
Config::with_cases(500),
|(
path in StandardPath::arbitrary_with(ArbitraryPathContext {
forwarding_key_generator: Some(Arc::new(StaticKeyGen)),
..Default::default()
}),
advance_seed in 0..255u8
)| {
test_imp(path, advance_seed)?;
}
);
fn test_imp(
path: StandardPath,
advance_seed: u8,
) -> Result<(), proptest::test_runner::TestCaseError> {
let mut view = path.try_encode_to_vec()?;
let (view, rest) = StandardPathView::try_from_mut_slice(view.as_mut_slice())?;
if !rest.is_empty() {
return Err(proptest::test_runner::TestCaseError::Fail(
"Encoded path has remaining bytes".into(),
));
}
let advance_count = advance_seed as usize % (view.hop_field_count() as usize - 1);
advance_path(view, Some(advance_count as u8))?;
view.try_reverse().expect(
"Reverse should succeed when we have advanced through the
path",
);
advance_path(view, None)?;
Ok(())
}
}
fn advance_path(
view: &mut StandardPathView,
max_steps: Option<u8>,
) -> Result<(), proptest::prelude::TestCaseError> {
if view.curr_hop_field_idx() == view.hop_field_count() - 1 {
return Ok(());
}
let static_key = StaticKeyGen::STATIC_KEY;
let validator = HopMacValidator { key: static_key };
view.advance_ingress_with_validator(validator.clone(), true)
.map_err(|e| {
proptest::test_runner::TestCaseError::Fail(
format!("First Advance ingress failed: {e:?}").into(),
)
})?
.into_result()
.map_err(|(output, err)| {
proptest::test_runner::TestCaseError::Fail(
format!("First Advance ingress validation failed: {err:?}, output: {output:?}")
.into(),
)
})?;
view.advance_egress_with_validator(validator.clone())
.map_err(|e| {
proptest::test_runner::TestCaseError::Fail(
format!("Second Advance egress failed: {e:?}").into(),
)
})?
.into_result()
.map_err(|(output, err)| {
proptest::test_runner::TestCaseError::Fail(
format!(
"Second Advance ingress validation failed: {err:?}, output: {output:?}"
)
.into(),
)
})?;
let mut steps = 1;
loop {
let validator = HopMacValidator { key: static_key };
let res = view
.advance_ingress_with_validator(validator.clone(), false)
.map_err(|e| {
proptest::test_runner::TestCaseError::Fail(
format!("Advance failed: {e:?}").into(),
)
})?
.into_result()
.map_err(|(output, err)| {
proptest::test_runner::TestCaseError::Fail(
format!("Advance ingress validation failed: {err:?}, output: {output:?}")
.into(),
)
})?;
match res.action {
IngressAdvanceAction::ContinueEgress { egress_if: _ } => {}
IngressAdvanceAction::ForwardLocal => {
break;
}
}
steps += 1;
if let Some(max) = max_steps
&& steps >= max
{
break;
}
view.advance_egress_with_validator(validator.clone())
.map_err(|e| {
proptest::test_runner::TestCaseError::Fail(
format!("Advance failed: {e:?}").into(),
)
})?
.into_result()
.map_err(|(output, err)| {
proptest::test_runner::TestCaseError::Fail(
format!("Advance egress validation failed: {err:?}, output: {output:?}")
.into(),
)
})?;
}
Ok(())
}
}