use std::str::FromStr;
use super::types::{HopPredicate, PathPolicyHop};
use crate::path::{ScionPath, policy::PathPolicy};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AclPolicy {
pub entries: Vec<AclEntry>,
pub default: AclEntryOperator,
}
impl AclPolicy {
#[inline]
pub const fn new(default: AclEntryOperator) -> Self {
Self {
entries: Vec::new(),
default,
}
}
#[inline]
pub fn new_from_entries(
default: AclEntryOperator,
entries: impl IntoIterator<Item = AclEntry>,
) -> Self {
Self {
entries: entries.into_iter().collect(),
default,
}
}
pub fn parse(s: &str) -> Result<Self, String> {
let mut entries = Vec::new();
let mut split = s.split_whitespace();
let mut first_op = split.next();
let mut second_op = split.next();
while let (Some(first), Some(second)) = (first_op, second_op) {
let op = AclEntryOperator::parse(first)?;
let hop = HopPredicate::from_str(second)?;
if hop.is_wildcard() {
if split.next().is_some() {
return Err("Wildcard hop predicate must be the last entry".into());
}
break;
}
entries.push(AclEntry::new(op, hop));
first_op = split.next();
second_op = split.next();
}
let Some(first) = first_op else {
return Err("Missing default operator".into());
};
let default = AclEntryOperator::parse(first)?;
Ok(Self { entries, default })
}
#[inline]
pub fn add_entry(mut self, operator: AclEntryOperator, hop: HopPredicate) -> Self {
self.entries.push(AclEntry::new(operator, hop));
self
}
pub fn matches(&self, path: &[PathPolicyHop]) -> bool {
if path.is_empty() || self.entries.is_empty() {
return self.default == AclEntryOperator::Allow;
}
for hop in path {
let mut hop_matched = false;
for entry in &self.entries {
match entry.matches(hop) {
AclMatchResult::Allow => {
hop_matched = true;
break;
}
AclMatchResult::Deny => return false,
AclMatchResult::Impartial => continue,
};
}
if !hop_matched && self.default == AclEntryOperator::Deny {
return false;
}
}
true
}
}
impl FromStr for AclPolicy {
type Err = String;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl PathPolicy for AclPolicy {
#[inline]
fn path_allowed(&self, path: &ScionPath) -> Result<bool, std::borrow::Cow<'static, str>> {
let path_hops = PathPolicyHop::hops_from_path(path)?;
Ok(self.matches(&path_hops))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AclEntry {
pub operator: AclEntryOperator,
pub hop_predicate: HopPredicate,
}
impl AclEntry {
#[inline]
pub const fn new(operator: AclEntryOperator, hop: HopPredicate) -> Self {
Self {
operator,
hop_predicate: hop,
}
}
#[inline]
pub fn parse(s: &str) -> Result<Self, String> {
let mut iter = s.splitn(2, ' ');
let operator = iter
.next()
.ok_or_else(|| "Missing operator".to_string())
.and_then(AclEntryOperator::parse)?;
let hop = iter
.next()
.ok_or_else(|| "Missing hop predicate".to_string())
.and_then(HopPredicate::from_str)?;
Ok(Self {
operator,
hop_predicate: hop,
})
}
#[inline]
fn matches(&self, hop: &PathPolicyHop) -> AclMatchResult {
if hop.matches(&self.hop_predicate) {
match self.operator {
AclEntryOperator::Allow => AclMatchResult::Allow,
AclEntryOperator::Deny => AclMatchResult::Deny,
}
} else {
AclMatchResult::Impartial
}
}
}
impl FromStr for AclEntry {
type Err = String;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum AclMatchResult {
Allow,
Deny,
Impartial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AclEntryOperator {
Allow,
Deny,
}
impl AclEntryOperator {
#[inline]
pub fn parse(s: &str) -> Result<Self, String> {
match s {
"+" => Ok(AclEntryOperator::Allow),
"-" => Ok(AclEntryOperator::Deny),
_ => Err(format!("Invalid operator: {s}")),
}
}
}
impl FromStr for AclEntryOperator {
type Err = String;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
#[cfg(test)]
mod test {
use super::*;
mod acl_policy {
use super::*;
fn expect_parse(s: &str) -> AclPolicy {
AclPolicy::parse(s).unwrap_or_else(|_| panic!("Should parse: {s}"))
}
#[test]
fn parse_valid_acl_succeeds() {
expect_parse("- 1 +");
expect_parse("- 1 + 0");
expect_parse("- 1 + 0-0");
expect_parse("- 1 + 0-0#0");
expect_parse("- 1 + 0-0#0,0");
expect_parse("- 1-2 +");
expect_parse("- 2 - 3-1#1,2 +");
expect_parse("- 1-2#1,2 +");
expect_parse("- 1 +");
expect_parse("- 1 - 2-2#1 +");
expect_parse("- 1 - 2-2#1,2 +");
}
fn expect_parse_fail(s: &str) -> String {
AclPolicy::parse(s).expect_err(&format!("Should fail with: {s}"))
}
#[test]
fn parse_invalid_acl_returns_error() {
expect_parse_fail(""); expect_parse_fail("- 1"); expect_parse_fail("1"); expect_parse_fail("+ +"); expect_parse_fail("- 1 + -"); expect_parse_fail("- 0-0 + 2 +"); }
#[test]
fn should_make_correct_decision() {
expect_decision("+", true, &[]); expect_decision("-", false, &[]); expect_decision("- 1 +", true, &[]);
expect_decision("+", true, &[hop(0, "1-3", 0)]); expect_decision("-", false, &[hop(0, "1-3", 0)]);
expect_decision("- 1-1 + 1 -", true, &[hop(0, "1-3", 0)]); expect_decision("+ 1-3 - 1 +", true, &[hop(0, "1-3", 0)]);
expect_decision("- 1 +", false, &[hop(0, "1-1", 0)]); expect_decision("- 1 +", true, &[hop(0, "2-1", 0)]);
expect_decision("- 1-2 +", false, &[hop(0, "1-2", 0)]); expect_decision("- 1-2 +", true, &[hop(0, "1-3", 0)]);
expect_decision("- 2 - 3-1#1,2 +", false, &[hop(1, "2-7", 2)]); expect_decision("- 2 - 3-1#1,2 +", false, &[hop(1, "3-1", 2)]); expect_decision("- 2 - 3-1#1,2 +", true, &[hop(2, "3-1", 1)]);
expect_decision("+ 2 + 1 -", true, &[hop(1, "2-7", 2)]); expect_decision("+ 2 + 1 -", true, &[hop(1, "1-7", 2)]); expect_decision("+ 2 + 1 -", false, &[hop(2, "3-1", 1)]); expect_decision("+ 2 + 1 -", true, &[hop(2, "1-7", 1), hop(2, "2-7", 1)]); expect_decision("+ 2 + 1 -", false, &[hop(2, "1-7", 1), hop(2, "3-1", 1)]);
expect_decision("- 1-2#1,2 +", false, &[hop(1, "1-2", 2)]); expect_decision("- 1-2#1,2 +", true, &[hop(2, "1-2", 1)]);
expect_decision("- 1 +", false, &[hop(0, "1-9", 0)]); expect_decision("- 1 +", true, &[hop(0, "9-1", 0)]);
expect_decision("- 1 - 2-2#1 +", false, &[hop(0, "1-3", 0)]); expect_decision("- 1 - 2-2#1 +", false, &[hop(1, "2-2", 0)]); expect_decision("- 1 - 2-2#1 +", true, &[hop(2, "2-2", 3)]);
expect_decision("- 1 - 2-2#1,2 +", false, &[hop(0, "1-3", 0)]); expect_decision("- 1 - 2-2#1,2 +", false, &[hop(1, "2-2", 2)]); expect_decision("- 1 - 2-2#1,2 +", true, &[hop(2, "2-2", 1)]);
expect_decision("- 1 +", false, &[hop(0, "2-1", 0), hop(0, "1-1", 0)]); expect_decision("- 1 +", true, &[hop(0, "2-1", 0), hop(0, "3-1", 0)]);
expect_decision("+ 1 -", true, &[hop(0, "1-1", 0), hop(0, "1-2", 0)]); expect_decision("+ 1 -", false, &[hop(0, "1-1", 0), hop(0, "2-1", 0)]); expect_decision("+ 1 -", false, &[hop(0, "2-1", 0), hop(0, "3-1", 0)]); expect_decision(
"- 1 +",
false,
&[hop(0, "1-1", 0), hop(0, "2-1", 0), hop(0, "3-1", 0)],
); expect_decision(
"- 4 +",
true,
&[hop(0, "1-1", 0), hop(0, "2-1", 0), hop(0, "3-1", 0)],
);
fn hop(isg: u16, isd_asn: &'static str, egress: u16) -> PathPolicyHop {
PathPolicyHop {
isd_asn: isd_asn.parse().unwrap(),
ingress: isg,
egress,
}
}
fn expect_decision(s: &str, expected: bool, path_hops: &[PathPolicyHop]) {
let acl = expect_parse(s);
let decision = acl.matches(path_hops);
assert_eq!(
decision, expected,
"ACL: {s}, Hops: {:?}, Expected: {}, Got: {}",
path_hops, expected, decision
);
}
}
}
}