use crate::{
bgp::{BgpRibEntry, Community},
ospf::LinkWeight,
types::{IntoIpv4Prefix, Ipv4Prefix, Prefix, PrefixSet, RouterId, ASN},
};
use ordered_float::NotNan;
use serde::{Deserialize, Serialize};
use std::{cmp::Ordering, fmt};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(bound(deserialize = "P: for<'a> serde::Deserialize<'a>"))]
pub struct RouteMap<P: Prefix> {
pub order: i16,
pub state: RouteMapState,
pub conds: Vec<RouteMapMatch<P>>,
pub set: Vec<RouteMapSet>,
pub flow: RouteMapFlow,
}
impl<P: Prefix> RouteMap<P> {
pub fn new(
order: i16,
state: RouteMapState,
conds: Vec<RouteMapMatch<P>>,
set: Vec<RouteMapSet>,
flow: RouteMapFlow,
) -> Self {
Self {
order,
state,
conds,
set,
flow,
}
}
pub fn apply(&self, mut route: BgpRibEntry<P>) -> (RouteMapFlow, Option<BgpRibEntry<P>>) {
match self.conds.iter().all(|c| c.matches(&route)) {
true => {
if self.state.is_deny() {
(RouteMapFlow::Exit, None)
} else {
self.set.iter().for_each(|s| s.apply(&mut route));
(self.flow, Some(route))
}
}
false => (RouteMapFlow::Continue, Some(route)), }
}
pub fn order(&self) -> i16 {
self.order
}
pub fn state(&self) -> RouteMapState {
self.state
}
pub fn conds(&self) -> &Vec<RouteMapMatch<P>> {
&self.conds
}
pub fn actions(&self) -> &Vec<RouteMapSet> {
&self.set
}
pub fn matches(&self, route: &BgpRibEntry<P>) -> bool {
self.conds.iter().all(|c| c.matches(route))
}
}
impl<P: Prefix> IntoIpv4Prefix for RouteMap<P> {
type T = RouteMap<Ipv4Prefix>;
fn into_ipv4_prefix(self) -> Self::T {
RouteMap {
order: self.order,
state: self.state,
conds: self
.conds
.into_iter()
.map(|x| x.into_ipv4_prefix())
.collect(),
set: self.set,
flow: self.flow,
}
}
}
pub trait RouteMapList<P: Prefix> {
fn apply(self, route: BgpRibEntry<P>) -> Option<BgpRibEntry<P>>;
}
impl<'a, P, I> RouteMapList<P> for I
where
P: Prefix + 'a,
I: IntoIterator<Item = &'a RouteMap<P>>,
{
fn apply(self, mut entry: BgpRibEntry<P>) -> Option<BgpRibEntry<P>> {
let mut wait_for = None;
for map in self {
if let Some(x) = wait_for {
match map.order.cmp(&x) {
Ordering::Less => continue,
Ordering::Equal => {}
Ordering::Greater => return Some(entry),
}
}
match map.apply(entry) {
(cont, Some(e)) => {
entry = e;
match cont {
RouteMapFlow::Exit => return Some(entry),
RouteMapFlow::Continue => wait_for = None,
RouteMapFlow::ContinueAt(x) => wait_for = Some(x),
}
}
(_, None) => return None,
}
}
Some(entry)
}
}
#[derive(Debug)]
pub struct RouteMapBuilder<P: Prefix> {
order: Option<i16>,
state: Option<RouteMapState>,
conds: Vec<RouteMapMatch<P>>,
set: Vec<RouteMapSet>,
prefix_conds: P::Set,
has_prefix_conds: bool,
flow: RouteMapFlow,
}
impl<P: Prefix> Default for RouteMapBuilder<P> {
fn default() -> Self {
Self {
order: None,
state: None,
conds: Vec::new(),
set: Vec::new(),
prefix_conds: Default::default(),
has_prefix_conds: false,
flow: RouteMapFlow::default(),
}
}
}
impl<P: Prefix> RouteMapBuilder<P> {
pub fn new() -> Self {
Self::default()
}
pub fn order(&mut self, order: u16) -> &mut Self {
self.order = Some(order as i16);
self
}
pub fn order_sgn(&mut self, order: i16) -> &mut Self {
self.order = Some(order);
self
}
pub fn state(&mut self, state: RouteMapState) -> &mut Self {
self.state = Some(state);
self
}
pub fn allow(&mut self) -> &mut Self {
self.state = Some(RouteMapState::Allow);
self
}
pub fn deny(&mut self) -> &mut Self {
self.state = Some(RouteMapState::Deny);
self
}
pub fn cond(&mut self, cond: RouteMapMatch<P>) -> &mut Self {
self.conds.push(cond);
self
}
pub fn match_prefix(&mut self, prefix: P) -> &mut Self {
self.prefix_conds.insert(prefix);
self.has_prefix_conds = true;
self
}
pub fn match_as_path_contains(&mut self, as_id: ASN) -> &mut Self {
self.conds
.push(RouteMapMatch::AsPath(RouteMapMatchAsPath::Contains(as_id)));
self
}
pub fn match_as_path_length(&mut self, as_path_len: usize) -> &mut Self {
self.conds
.push(RouteMapMatch::AsPath(RouteMapMatchAsPath::Length(
RouteMapMatchClause::Equal(as_path_len),
)));
self
}
pub fn match_as_path_length_range(&mut self, from: usize, to: usize) -> &mut Self {
self.conds
.push(RouteMapMatch::AsPath(RouteMapMatchAsPath::Length(
RouteMapMatchClause::Range(from, to),
)));
self
}
pub fn match_next_hop(&mut self, next_hop: RouterId) -> &mut Self {
self.conds.push(RouteMapMatch::NextHop(next_hop));
self
}
pub fn match_community(&mut self, community: impl Into<Community>) -> &mut Self {
self.conds.push(RouteMapMatch::Community(community.into()));
self
}
pub fn match_deny_community(&mut self, community: impl Into<Community>) -> &mut Self {
self.conds
.push(RouteMapMatch::DenyCommunity(community.into()));
self
}
pub fn add_set(&mut self, set: RouteMapSet) -> &mut Self {
self.set.push(set);
self
}
pub fn set_next_hop(&mut self, next_hop: RouterId) -> &mut Self {
self.set.push(RouteMapSet::NextHop(next_hop));
self
}
pub fn set_weight(&mut self, weight: u32) -> &mut Self {
self.set.push(RouteMapSet::Weight(Some(weight)));
self
}
pub fn reset_weight(&mut self) -> &mut Self {
self.set.push(RouteMapSet::Weight(None));
self
}
pub fn set_local_pref(&mut self, local_pref: u32) -> &mut Self {
self.set.push(RouteMapSet::LocalPref(Some(local_pref)));
self
}
pub fn reset_local_pref(&mut self) -> &mut Self {
self.set.push(RouteMapSet::LocalPref(None));
self
}
pub fn set_med(&mut self, med: u32) -> &mut Self {
self.set.push(RouteMapSet::Med(Some(med)));
self
}
pub fn reset_med(&mut self) -> &mut Self {
self.set.push(RouteMapSet::Med(None));
self
}
pub fn set_igp_cost(&mut self, cost: LinkWeight) -> &mut Self {
self.set.push(RouteMapSet::IgpCost(cost));
self
}
pub fn set_community(&mut self, community: impl Into<Community>) -> &mut Self {
self.set.push(RouteMapSet::SetCommunity(community.into()));
self
}
pub fn remove_community(&mut self, community: impl Into<Community>) -> &mut Self {
self.set.push(RouteMapSet::DelCommunity(community.into()));
self
}
pub fn prepend_asn(&mut self, asn: impl Into<ASN>) -> &mut Self {
self.set.push(RouteMapSet::PrependASPath(vec![asn.into()]));
self
}
pub fn prepend_as_path<I>(&mut self, as_path: I) -> &mut Self
where
I: IntoIterator,
I::Item: Into<ASN>,
{
self.set.push(RouteMapSet::PrependASPath(
as_path.into_iter().map(|x| x.into()).collect(),
));
self
}
pub fn exit(&mut self) -> &mut Self {
self.flow = RouteMapFlow::Exit;
self
}
pub fn continue_next(&mut self) -> &mut Self {
self.flow = RouteMapFlow::Continue;
self
}
pub fn continue_at(&mut self, order: i16) -> &mut Self {
self.flow = RouteMapFlow::ContinueAt(order);
self
}
pub fn build(&self) -> RouteMap<P> {
let order = match self.order {
Some(o) => o,
None => panic!("Order was not set for a Route-Map!"),
};
let state = match self.state {
Some(s) => s,
None => panic!("State was not set for a Route-Map!"),
};
if let RouteMapFlow::ContinueAt(continue_at) = self.flow {
assert!(
continue_at > order,
"The order of the next route map must be larger than the order!"
);
}
let mut conds = self.conds.clone();
if self.has_prefix_conds {
conds.push(RouteMapMatch::Prefix(self.prefix_conds.clone()));
}
let set = if state.is_deny() {
vec![]
} else {
self.set.clone()
};
RouteMap::new(order, state, conds, set, self.flow)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RouteMapState {
Allow,
Deny,
}
impl RouteMapState {
pub fn is_allow(&self) -> bool {
self == &Self::Allow
}
pub fn is_deny(&self) -> bool {
self == &Self::Deny
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RouteMapMatch<P: Prefix> {
Prefix(P::Set),
AsPath(RouteMapMatchAsPath),
NextHop(RouterId),
Community(Community),
DenyCommunity(Community),
}
impl<P: Prefix> IntoIpv4Prefix for RouteMapMatch<P> {
type T = RouteMapMatch<Ipv4Prefix>;
fn into_ipv4_prefix(self) -> Self::T {
match self {
RouteMapMatch::Prefix(x) => {
RouteMapMatch::Prefix(x.into_iter().map(Prefix::into_ipv4_prefix).collect())
}
RouteMapMatch::AsPath(x) => RouteMapMatch::AsPath(x),
RouteMapMatch::NextHop(x) => RouteMapMatch::NextHop(x),
RouteMapMatch::Community(x) => RouteMapMatch::Community(x),
RouteMapMatch::DenyCommunity(x) => RouteMapMatch::DenyCommunity(x),
}
}
}
impl<P: Prefix> RouteMapMatch<P> {
pub fn matches(&self, entry: &BgpRibEntry<P>) -> bool {
match self {
Self::Prefix(prefixes) => prefixes.contains(&entry.route.prefix),
Self::AsPath(clause) => clause.matches(&entry.route.as_path),
Self::NextHop(nh) => entry.route.next_hop == *nh,
Self::Community(com) => entry.route.community.contains(com),
Self::DenyCommunity(com) => !entry.route.community.contains(com),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RouteMapMatchClause<T> {
Range(T, T),
RangeExclusive(T, T),
Equal(T),
}
impl<T> RouteMapMatchClause<T>
where
T: PartialOrd + PartialEq,
{
pub fn matches(&self, val: &T) -> bool {
match self {
Self::Range(min, max) => val >= min && val <= max,
Self::RangeExclusive(min, max) => val >= min && val < max,
Self::Equal(x) => val == x,
}
}
}
impl<T> fmt::Display for RouteMapMatchClause<T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RouteMapMatchClause::Range(a, b) => f.write_fmt(format_args!("in ({a}..{b})")),
RouteMapMatchClause::RangeExclusive(a, b) => {
f.write_fmt(format_args!("in ({a}..{b}])"))
}
RouteMapMatchClause::Equal(a) => f.write_fmt(format_args!("== {a}")),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RouteMapMatchAsPath {
Contains(ASN),
Length(RouteMapMatchClause<usize>),
}
impl RouteMapMatchAsPath {
pub fn matches(&self, path: &[ASN]) -> bool {
match self {
Self::Contains(as_id) => path.contains(as_id),
Self::Length(clause) => clause.matches(&path.len()),
}
}
}
impl fmt::Display for RouteMapMatchAsPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RouteMapMatchAsPath::Contains(as_id) => {
f.write_fmt(format_args!("{} in AsPath", as_id.0))
}
RouteMapMatchAsPath::Length(c) => f.write_fmt(format_args!("len(AsPath) {c}")),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum RouteMapSet {
NextHop(RouterId),
Weight(Option<u32>),
LocalPref(Option<u32>),
Med(Option<u32>),
IgpCost(LinkWeight),
SetCommunity(Community),
DelCommunity(Community),
PrependASPath(Vec<ASN>),
}
impl RouteMapSet {
pub fn apply<P: Prefix>(&self, entry: &mut BgpRibEntry<P>) {
match self {
Self::NextHop(nh) => {
entry.route.next_hop = *nh;
entry.igp_cost = None
}
Self::Weight(w) => entry.weight = w.unwrap_or(100),
Self::LocalPref(lp) => entry.route.local_pref = Some(lp.unwrap_or(100)),
Self::Med(med) => entry.route.med = Some(med.unwrap_or(0)),
Self::IgpCost(w) => entry.igp_cost = Some(NotNan::new(*w).unwrap()),
Self::SetCommunity(c) => {
entry.route.community.insert(*c);
}
Self::DelCommunity(c) => {
entry.route.community.remove(c);
}
Self::PrependASPath(asns) => {
entry.route.as_path = asns.iter().chain(&entry.route.as_path).copied().collect()
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum RouteMapDirection {
Incoming,
Outgoing,
}
impl fmt::Display for RouteMapDirection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RouteMapDirection::Incoming => write!(f, "in"),
RouteMapDirection::Outgoing => write!(f, "out"),
}
}
}
impl RouteMapDirection {
pub fn incoming(&self) -> bool {
matches!(self, Self::Incoming)
}
pub fn outgoing(&self) -> bool {
matches!(self, Self::Outgoing)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum RouteMapFlow {
Exit,
Continue,
ContinueAt(i16),
}
impl Default for RouteMapFlow {
fn default() -> Self {
Self::Continue
}
}
impl fmt::Display for RouteMapFlow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RouteMapFlow::Exit => write!(f, "break"),
RouteMapFlow::Continue => write!(f, "continue"),
RouteMapFlow::ContinueAt(c) => write!(f, "continue at {c}"),
}
}
}
impl<P: Prefix> RouteMap<P> {
pub fn from_str(
order: i16,
cond: impl AsRef<str>,
action: impl AsRef<str>,
flow: RouteMapFlow,
) -> Result<Self, String> {
let conds = parser::parse_cond(cond.as_ref())?;
let actions = parser::parse_action(action.as_ref())?;
let (state, set) = match actions {
Some(set) => (RouteMapState::Allow, set),
None => (RouteMapState::Deny, Vec::new()),
};
Ok(Self::new(order, state, conds, set, flow))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn from_str() {
let got = RouteMap::<Ipv4Prefix>::from_str(
10,
"community 1:1",
"lp 200; comm set 1:100",
Default::default(),
)
.unwrap();
let want = RouteMapBuilder::new()
.order(10)
.allow()
.match_community((1, 1))
.set_local_pref(200)
.set_community((1, 100))
.build();
assert_eq!(got, want);
}
#[test]
fn from_str_no_action() {
let got =
RouteMap::<Ipv4Prefix>::from_str(10, "community 1:1", "", Default::default()).unwrap();
let want = RouteMapBuilder::new()
.order(10)
.allow()
.match_community((1, 1))
.build();
assert_eq!(got, want);
}
#[test]
fn from_str_no_action_permit() {
let got =
RouteMap::<Ipv4Prefix>::from_str(10, "community 1:1", "permit", Default::default())
.unwrap();
let want = RouteMapBuilder::new()
.order(10)
.allow()
.match_community((1, 1))
.build();
assert_eq!(got, want);
}
#[test]
fn from_str_deny() {
let got = RouteMap::<Ipv4Prefix>::from_str(10, "community 1:1", "deny", Default::default())
.unwrap();
let want = RouteMapBuilder::new()
.order(10)
.deny()
.match_community((1, 1))
.build();
assert_eq!(got, want);
}
#[test]
fn from_str_no_match() {
let got =
RouteMap::<Ipv4Prefix>::from_str(10, "*", "lp 200; comm set 1:100", Default::default())
.unwrap();
let want = RouteMapBuilder::new()
.order(10)
.allow()
.set_local_pref(200)
.set_community((1, 100))
.build();
assert_eq!(got, want);
}
}
mod parser {
use ipnet::Ipv4Net;
use super::{RouteMapMatch, RouteMapMatchAsPath, RouteMapSet};
use crate::{
bgp::Community,
types::{Prefix, ASN},
};
trait StrResult<T> {
fn context(self, context: impl std::fmt::Display) -> Result<T, String>;
}
impl<T, E: std::fmt::Display> StrResult<T> for Result<T, E> {
fn context(self, context: impl std::fmt::Display) -> Result<T, String> {
self.map_err(|e| format!("{context}: {e}"))
}
}
impl<T> StrResult<T> for Option<T> {
fn context(self, context: impl ToString) -> Result<T, String> {
self.ok_or_else(|| context.to_string())
}
}
pub(super) fn parse_cond<P: Prefix>(cond: &str) -> Result<Vec<RouteMapMatch<P>>, String> {
let cond = cond.trim().to_lowercase();
if cond == "*" || cond == "any" || cond == "all" || cond == "" {
return Ok(Vec::new());
}
cond.split(";")
.map(|s| s.trim())
.filter(|x| !x.is_empty())
.map(parse_single_cond)
.collect()
}
fn parse_single_cond<P: Prefix>(cond: &str) -> Result<RouteMapMatch<P>, String> {
let mut words = cond.split_whitespace();
let kind = words.next().context("Got an empty condition")?;
match kind {
"prefix" => Ok(RouteMapMatch::Prefix(
words
.map(|x| {
x.parse::<Ipv4Net>()
.context(format!("Failed to parse `{x}` as `Ipv4Net`"))
.map(|x| P::from(x))
})
.collect::<Result<_, String>>()?,
)),
"community" | "comm" => {
let mut comm = words
.next()
.context("Community requires at least one argument")?;
let mut positive = true;
if comm == "not" {
positive = !positive;
comm = words
.next()
.context("Negated community requires one argument")?;
}
let comm = comm
.parse::<Community>()
.context(format!("Cannot parse `{comm}` as `Community`"))?;
Ok(if positive {
RouteMapMatch::Community(comm)
} else {
RouteMapMatch::DenyCommunity(comm)
})
}
"aspath" | "path" => {
let kind = words
.next()
.context("`aspath` requires at least one additional argument")?;
match kind {
"contains" => Ok(RouteMapMatch::AsPath(RouteMapMatchAsPath::Contains(ASN(
words
.next()
.context("`aspath contains` requires at least one additional argument")?
.parse::<u32>()
.context("Cannot parse the argument as number")?,
)))),
s => Err(format!("Unsupported argument to `aspath`: `{s}`")),
}
}
s => Err(format!("Unsupported match kind: `{s}`")),
}
}
pub(super) fn parse_action(action: &str) -> Result<Option<Vec<RouteMapSet>>, String> {
let action = action.trim().to_lowercase();
if action == "drop" || action == "deny" || action == "reject" || action == "discard" {
return Ok(None);
}
if action == "permit" || action == "accept" || action == "" {
return Ok(Some(Vec::new()));
}
Ok(Some(
action
.split(";")
.map(|s| s.trim())
.filter(|x| !x.is_empty())
.map(parse_single_action)
.collect::<Result<Vec<_>, String>>()?,
))
}
fn parse_single_action(cond: &str) -> Result<RouteMapSet, String> {
let mut words = cond.split_whitespace();
let kind = words.next().context("Got an empty action")?;
match kind {
"local-preference" | "lp" | "local-pref" | "localpreference" | "localpref" => {
let num = words
.next()
.context("`local-preference` expects one argument")?;
Ok(RouteMapSet::LocalPref(Some(
num.parse::<u32>()
.context(format!("cannot parse `{num}` as a number"))?,
)))
}
"med" => {
let num = words.next().context("`med` expects one argument")?;
Ok(RouteMapSet::Med(Some(
num.parse::<u32>()
.context(format!("cannot parse `{num}` as a number"))?,
)))
}
"community" | "comm" => {
let kind = words
.next()
.context("`community` requires two arguments.")?;
let comm = words
.next()
.context("`community` requires two arguments.")?;
let comm = comm
.parse::<Community>()
.context(format!("cannot pare `{comm}` as `Community`."))?;
match kind {
"set" | "add" => Ok(RouteMapSet::SetCommunity(comm)),
"del" => Ok(RouteMapSet::DelCommunity(comm)),
s => Err(format!(
"Invalid first argument for `community`: `{s}` (expecting `set` or `del`)"
)),
}
}
"aspath" | "path" => {
let kind = words
.next()
.context("`aspath` requires at least one argument.")?;
match kind {
"prepend" => Ok(RouteMapSet::PrependASPath(
words
.map(|x| {
x.parse::<u32>()
.context(format!("Cannot parse `{x}` as a number"))
.map(ASN)
})
.collect::<Result<Vec<_>, String>>()?,
)),
s => Err(format!(
"Invalid first argument for `aspath`: `{s}` (expecting `prepend`)"
)),
}
}
s => Err(format!("Unsupported action kind: `{s}`")),
}
}
#[cfg(test)]
mod test {
use crate::types::Ipv4Prefix;
use super::*;
#[test]
fn test_parse_single_cond() {
assert!(parse_single_cond::<Ipv4Prefix>("prefix 192.168.1.0/24").is_ok());
assert!(parse_single_cond::<Ipv4Prefix>("community 65000:1").is_ok());
assert!(parse_single_cond::<Ipv4Prefix>("comm 65000:1").is_ok());
assert!(parse_single_cond::<Ipv4Prefix>("community not 65000:1").is_ok());
assert!(parse_single_cond::<Ipv4Prefix>("path contains 65000").is_ok());
assert!(parse_single_cond::<Ipv4Prefix>("aspath contains 65000").is_ok());
}
#[test]
fn test_parse_single_cond_errors() {
assert!(parse_single_cond::<Ipv4Prefix>("prefix invalid_ip").is_err());
assert!(parse_single_cond::<Ipv4Prefix>("community").is_err()); assert!(parse_single_cond::<Ipv4Prefix>("aspath somethingelse 65000").is_err());
assert!(parse_single_cond::<Ipv4Prefix>("unknown_kind 123").is_err());
}
#[test]
fn test_parse_single_action_valid() {
assert!(parse_single_action("lp 100").is_ok());
assert!(parse_single_action("med 50").is_ok());
assert!(parse_single_action("comm set 65000:100").is_ok());
assert!(parse_single_action("comm set BLACKHOLE").is_ok());
assert!(parse_single_action("community del 65000:100").is_ok());
assert!(parse_single_action("aspath prepend 65001 65002").is_ok());
}
#[test]
fn test_parse_single_action_errors() {
assert!(parse_single_action("lp not_a_number").is_err());
assert!(parse_single_action("med").is_err()); assert!(parse_single_action("comm replace 65000:1").is_err()); assert!(parse_single_action("comm set 65000").is_err()); assert!(parse_single_action("comm set foo").is_err()); assert!(parse_single_action("aspath append 65000").is_err()); assert!(parse_single_action("aspath prepend foo 5441").is_err());
}
}
}