use core::fmt;
use std::cmp;
use std::net;
use inetnum::asn::Asn;
use log::trace;
use super::{
aspath::HopPath,
path_attributes::{BgpIdentifier, ClusterIds, PaMap},
types::{LocalPref, MultiExitDisc, Origin, OriginatorId},
};
#[derive(Copy, Clone, Debug)]
pub struct OrdRoute<'a, OS> {
pa_map: &'a PaMap,
tiebreakers: TiebreakerInfo,
_strategy: std::marker::PhantomData<OS>,
}
impl<OS> OrdRoute<'_, OS> {
fn eligible(self) -> Result<Self, DecisionError> {
use DecisionErrorType as DET;
if !self.pa_map.contains::<Origin>() {
return Err(DET::MissingOrigin.into());
}
if !self.pa_map.contains::<HopPath>() {
return Err(DET::MissingAsPath.into());
}
if self.tiebreakers.source == RouteSource::Ebgp
&& self
.pa_map
.get::<HopPath>()
.and_then(|asp| asp.neighbor_path_selection())
.is_none()
{
return Err(DET::EbgpWithoutNeighbour.into());
}
Ok(self)
}
pub fn tiebreakers(&self) -> TiebreakerInfo {
self.tiebreakers
}
pub fn pa_map(&self) -> &PaMap {
self.pa_map
}
pub fn inner(&self) -> (TiebreakerInfo, &PaMap) {
(self.tiebreakers, self.pa_map())
}
}
impl<'a, OS: OrdStrat> OrdRoute<'a, OS> {
pub fn try_new(
pa_map: &'a PaMap,
tiebreakers: TiebreakerInfo,
) -> Result<Self, DecisionError> {
Self {
pa_map,
tiebreakers,
_strategy: std::marker::PhantomData,
}
.eligible()
}
}
impl<'a> OrdRoute<'a, Rfc4271> {
pub fn rfc4271(
pa_map: &'a PaMap,
tiebreakers: TiebreakerInfo,
) -> Result<Self, DecisionError> {
Self::try_new(pa_map, tiebreakers)
}
}
impl<'a> OrdRoute<'a, SkipMed> {
pub fn skip_med(
pa_map: &'a PaMap,
tiebreakers: TiebreakerInfo,
) -> Result<Self, DecisionError> {
Self::try_new(pa_map, tiebreakers)
}
}
impl<'a, T> OrdRoute<'a, T> {
pub fn into_strat<U: OrdStrat>(self) -> OrdRoute<'a, U> {
OrdRoute {
pa_map: self.pa_map,
tiebreakers: self.tiebreakers,
_strategy: std::marker::PhantomData,
}
}
pub fn from_strat<U: OrdStrat>(src: OrdRoute<'a, U>) -> Self {
OrdRoute {
pa_map: src.pa_map,
tiebreakers: src.tiebreakers,
_strategy: std::marker::PhantomData,
}
}
}
pub trait OrdStrat {
fn step_c<OS>(a: &OrdRoute<OS>, b: &OrdRoute<OS>) -> cmp::Ordering;
#[allow(unused_variables)]
fn step_e<OS>(a: &OrdRoute<OS>, b: &OrdRoute<OS>) -> cmp::Ordering {
cmp::Ordering::Equal
}
}
#[derive(Copy, Clone, Debug)]
pub struct Rfc4271;
impl OrdStrat for Rfc4271 {
fn step_c<OS>(a: &OrdRoute<OS>, b: &OrdRoute<OS>) -> cmp::Ordering {
if a.pa_map
.get::<HopPath>()
.and_then(|asp| asp.neighbor_path_selection())
.unwrap_or(a.tiebreakers.local_asn)
== b.pa_map
.get::<HopPath>()
.and_then(|asp| asp.neighbor_path_selection())
.unwrap_or(b.tiebreakers.local_asn)
{
trace!("checking MEDs");
let a_med =
a.pa_map.get::<MultiExitDisc>().unwrap_or(MultiExitDisc(0));
let b_med =
b.pa_map.get::<MultiExitDisc>().unwrap_or(MultiExitDisc(0));
a_med.cmp(&b_med)
} else {
cmp::Ordering::Equal
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct SkipMed;
impl OrdStrat for SkipMed {
fn step_c<OS>(_a: &OrdRoute<OS>, _b: &OrdRoute<OS>) -> cmp::Ordering {
std::cmp::Ordering::Equal
}
}
impl<OS: OrdStrat> PartialOrd for OrdRoute<'_, OS> {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<OS: OrdStrat> Ord for OrdRoute<'_, OS> {
fn cmp(&self, other: &Self) -> cmp::Ordering {
let a_dop = self
.tiebreakers
.degree_of_preference
.or_else(|| {
if self.tiebreakers.source == RouteSource::Ibgp {
self.pa_map.get::<LocalPref>().map(Into::into)
} else {
None
}
})
.unwrap_or(DegreeOfPreference(0));
let b_dop = other
.tiebreakers
.degree_of_preference
.or_else(|| {
if other.tiebreakers.source == RouteSource::Ibgp {
other.pa_map.get::<LocalPref>().map(Into::into)
} else {
None
}
})
.unwrap_or(DegreeOfPreference(0));
b_dop
.cmp(&a_dop)
.then_with(|| {
trace!("equal after DoP");
match (
self.pa_map.get::<HopPath>(),
other.pa_map.get::<HopPath>(),
) {
(Some(a), Some(b)) => a
.hop_count_path_selection()
.cmp(&b.hop_count_path_selection()),
(_, _) => {
panic!("can not compare routes lacking AS_PATH");
}
}
})
.then_with(|| {
trace!("equal after step a");
match (
self.pa_map.get::<Origin>(),
other.pa_map.get::<Origin>(),
) {
(Some(a), Some(b)) => a.cmp(&b),
(_, _) => cmp::Ordering::Equal,
}
})
.then_with(|| {
trace!("equal after step b");
OS::step_c(self, other)
})
.then_with(|| {
trace!("equal after step c");
self.tiebreakers.source.cmp(&other.tiebreakers.source)
})
.then_with(|| {
OS::step_e(self, other)
})
.then_with(|| {
trace!("equal after step d+e");
self.pa_map
.get::<OriginatorId>()
.map(|id| id.0.octets())
.unwrap_or(self.tiebreakers.bgp_identifier.into())
.cmp(
&other
.pa_map
.get::<OriginatorId>()
.map(|id| id.0.octets())
.unwrap_or(
other.tiebreakers.bgp_identifier.into(),
),
)
})
.then_with(|| {
trace!("equal after step f");
self.pa_map
.get::<ClusterIds>()
.map(|l| l.len())
.unwrap_or(0)
.cmp(
&other
.pa_map
.get::<ClusterIds>()
.map(|l| l.len())
.unwrap_or(0),
)
})
.then_with(|| {
trace!("equal after step f2");
self.tiebreakers.peer_addr.cmp(&other.tiebreakers.peer_addr)
})
.then_with(|| {
trace!("equal at end of tie breakers");
cmp::Ordering::Equal
})
}
}
impl<OS: OrdStrat> Eq for OrdRoute<'_, OS> {}
impl<OS: OrdStrat> PartialEq for OrdRoute<'_, OS> {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct TiebreakerInfo {
source: RouteSource,
degree_of_preference: Option<DegreeOfPreference>,
local_asn: Asn,
bgp_identifier: BgpIdentifier,
peer_addr: net::IpAddr,
}
impl TiebreakerInfo {
pub fn new(
source: RouteSource,
degree_of_preference: Option<DegreeOfPreference>,
local_asn: Asn,
bgp_identifier: BgpIdentifier,
peer_addr: net::IpAddr,
) -> Self {
Self {
source,
degree_of_preference,
local_asn,
bgp_identifier,
peer_addr,
}
}
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Ord, PartialOrd)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub enum RouteSource {
Ebgp,
Ibgp,
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Ord, PartialOrd)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct DegreeOfPreference(pub u32);
impl From<LocalPref> for DegreeOfPreference {
fn from(value: LocalPref) -> Self {
Self(value.0)
}
}
#[derive(Copy, Clone, Debug)]
pub struct DecisionError {
error_type: DecisionErrorType,
}
impl fmt::Display for DecisionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.error_type, f)
}
}
impl std::error::Error for DecisionError {}
#[allow(unused)]
#[derive(Copy, Clone, Debug)]
enum DecisionErrorType {
EbgpWithoutNeighbour,
AsPathLoop,
MissingAsPath,
MissingOrigin,
}
impl From<DecisionErrorType> for DecisionError {
fn from(det: DecisionErrorType) -> Self {
DecisionError { error_type: det }
}
}
impl fmt::Display for DecisionErrorType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use DecisionErrorType as DET;
match self {
DET::EbgpWithoutNeighbour => {
write!(f, "expected non-empty AS_PATH")
}
DET::AsPathLoop => write!(f, "AS_PATH loop"),
DET::MissingAsPath => write!(f, "missing mandatory AS_PATH"),
DET::MissingOrigin => write!(f, "missing mandatory ORIGIN"),
}
}
}
pub fn preferred<T: Ord>(a: T, b: T) -> T {
cmp::min(a,b)
}
pub fn best<T, I>(it: I) -> Option<T>
where
T: Ord,
I: Iterator<Item = T>,
{
it.min()
}
pub fn best_backup_generic<I, T>(it: I) -> (Option<T>, Option<T>)
where
I: Iterator<Item = T>,
T: Ord
{
let mut best = None;
let mut backup = None;
for c in it {
match best.take() {
None => { best = Some(c); continue }
Some(cur_best) => {
if c < cur_best {
best = Some(c);
backup = Some(cur_best);
continue;
}
best = Some(cur_best);
match backup.take() {
None => { backup = Some(c); }
Some(cur_backup) => {
if c < cur_backup {
backup = Some(c);
} else {
backup = Some(cur_backup);
}
}
}
}
}
}
(best, backup)
}
type RouteWithIndex<T> = (usize, T);
fn _best_backup<'a, I, T, OS>(it: I)
-> (Option<RouteWithIndex<T>>, Option<RouteWithIndex<T>>)
where
OS: OrdStrat,
I: Iterator<Item = T>,
T: Ord + core::borrow::Borrow<OrdRoute<'a, OS>>
{
let mut best: Option<RouteWithIndex<T>> = None;
let mut backup: Option<RouteWithIndex<T>> = None;
for (idx, c) in it.enumerate() {
match best.take() {
None => { best = Some((idx, c)); continue }
Some((idx_best, cur_best)) => {
if c < cur_best {
best = Some((idx, c));
backup = Some((idx_best, cur_best));
continue;
}
best = Some((idx_best, cur_best));
match backup.take() {
None => {
if let Some((_, cur_best)) = best.as_ref() {
if cur_best.borrow().inner() != c.borrow().inner()
{
backup = Some((idx, c));
}
}
}
Some((idx_backup, cur_backup)) => {
if c < cur_backup {
if best.as_ref().map(|t| &t.1) != Some(&c) {
backup = Some((idx, c));
continue;
}
}
backup = Some((idx_backup, cur_backup));
}
}
}
}
}
(best, backup)
}
pub fn best_backup<'a, I, T, OS>(it: I) -> (Option<T>, Option<T>)
where
OS: OrdStrat,
I: Iterator<Item = T>,
T: Ord + core::borrow::Borrow<OrdRoute<'a, OS>>
{
let (best, backup) = _best_backup(it);
(best.map(|b| b.1), backup.map(|b| b.1))
}
pub fn best_backup_position<'a, I, T, OS>(it: I)
-> (Option<usize>, Option<usize>)
where
OS: OrdStrat,
I: Iterator<Item = T>,
T: Ord + core::borrow::Borrow<OrdRoute<'a, OS>>
{
let (best, backup) = _best_backup(it);
(best.map(|b| b.0), backup.map(|b| b.0))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bgp::types::OriginType;
#[test]
fn route_source_order() {
assert!(RouteSource::Ebgp < RouteSource::Ibgp);
}
#[test]
fn rfc4271_total_ordering_fail() {
let tiebreakers = TiebreakerInfo {
source: RouteSource::Ebgp,
degree_of_preference: None,
local_asn: Asn::from_u32(100),
bgp_identifier: [0, 0, 0, 0].into(),
peer_addr: "::".parse().unwrap(),
};
let mut a_pamap = PaMap::empty();
a_pamap.set(Origin(OriginType::Egp));
a_pamap.set(HopPath::from([10, 20, 30]));
a_pamap.set(MultiExitDisc(100));
let mut b_pamap = PaMap::empty();
b_pamap.set(Origin(OriginType::Egp));
b_pamap.set(HopPath::from([10, 25, 30]));
b_pamap.set(MultiExitDisc(50));
let mut c_pamap = PaMap::empty();
c_pamap.set(Origin(OriginType::Egp));
c_pamap.set(HopPath::from([80, 90, 100]));
let a = OrdRoute::rfc4271(&a_pamap, tiebreakers).unwrap();
let b = OrdRoute::rfc4271(&b_pamap, tiebreakers).unwrap();
let c = OrdRoute::rfc4271(&c_pamap, tiebreakers).unwrap();
assert!(a > b);
assert!(b == c);
assert!(a == c);
let a: OrdRoute<SkipMed> = a.into_strat();
let b: OrdRoute<SkipMed> = b.into_strat();
let c: OrdRoute<SkipMed> = c.into_strat();
assert!(a == b);
assert!(b == c);
assert!(a == c);
}
#[test]
fn helpers_ordroute() {
let tiebreakers = TiebreakerInfo {
source: RouteSource::Ebgp,
degree_of_preference: None,
local_asn: Asn::from_u32(100),
bgp_identifier: [0, 0, 0, 0].into(),
peer_addr: "::".parse().unwrap(),
};
let mut a_pamap = PaMap::empty();
a_pamap.set(Origin(OriginType::Egp));
a_pamap.set(HopPath::from([10, 20, 30]));
a_pamap.set(MultiExitDisc(100));
let mut b_pamap = PaMap::empty();
b_pamap.set(Origin(OriginType::Egp));
b_pamap.set(HopPath::from([10, 25, 30, 40]));
b_pamap.set(MultiExitDisc(50));
let mut c_pamap = PaMap::empty();
c_pamap.set(Origin(OriginType::Egp));
c_pamap.set(HopPath::from([80, 90, 100, 200, 300]));
let candidates = [
OrdRoute::skip_med(&a_pamap, tiebreakers).unwrap(),
OrdRoute::skip_med(&b_pamap, tiebreakers).unwrap(),
OrdRoute::skip_med(&a_pamap, tiebreakers).unwrap(),
];
let best1 = best(candidates.iter().cloned()).unwrap();
let (best2, backup2) = best_backup(candidates.iter());
let (best2, backup2) = (best2.unwrap(), backup2.unwrap());
assert_eq!(best1.pa_map(), best2.pa_map());
assert_eq!(best1.tiebreakers(), best2.tiebreakers());
dbg!(&best2);
dbg!(&backup2);
assert_ne!(
(best2.pa_map(), best2.tiebreakers()),
(backup2.pa_map(), backup2.tiebreakers())
);
}
}