mirror of
https://github.com/NLnetLabs/krill.git
synced 2026-09-23 01:44:54 +02:00
375 lines
11 KiB
Rust
375 lines
11 KiB
Rust
//! Autonomous System Provider Authorizations.
|
||
//!
|
||
//! This is still being discussed in the IETF. No RFC just yet.
|
||
//! See the drafts
|
||
//! <https://datatracker.ietf.org/doc/draft-ietf-sidrops-aspa-profile/> and
|
||
//! <https://datatracker.ietf.org/doc/draft-ietf-sidrops-aspa-verification/>
|
||
|
||
use std::{error, fmt};
|
||
use std::str::FromStr;
|
||
use rpki::repository::resources::Asn;
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
|
||
//------------- Type Aliases -------------------------------------------------
|
||
|
||
/// The type of a customer ASN.
|
||
//
|
||
// *Warning:* This type is used in stored state.
|
||
pub type CustomerAsn = Asn;
|
||
|
||
/// The type of a provider ASN.
|
||
//
|
||
// *Warning:* This type is used in stored state.
|
||
pub type ProviderAsn = Asn;
|
||
|
||
|
||
//------------ AspaDefinitionUpdates -----------------------------------------
|
||
|
||
/// Information for an ASPA definition update.
|
||
//
|
||
// *Warning:* This type is used in stored state.
|
||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||
pub struct AspaDefinitionUpdates {
|
||
/// Definitions to add or replace.
|
||
///
|
||
/// Definitions in this list will be added if their customer ASN doesn’t
|
||
/// exist yet or will be updated if they do.
|
||
pub add_or_replace: Vec<AspaDefinition>,
|
||
|
||
/// Definitions to remove.
|
||
///
|
||
/// Definitions with these customer ASNs will be removed.
|
||
pub remove: Vec<CustomerAsn>,
|
||
}
|
||
|
||
impl fmt::Display for AspaDefinitionUpdates {
|
||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
write!(f, "Update ASPA definitions: ")?;
|
||
if !self.add_or_replace.is_empty() {
|
||
write!(f, " add or replace:")?;
|
||
for definition in &self.add_or_replace {
|
||
write!(f, " {definition}")?;
|
||
}
|
||
}
|
||
if !self.remove.is_empty() {
|
||
write!(f, " remove where customer ASN is:")?;
|
||
for as_id in &self.remove {
|
||
write!(f, " {as_id}")?;
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
|
||
//------------ AspaDefinitionList ----------------------------------------
|
||
|
||
/// A list of ASPA definitions.
|
||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||
pub struct AspaDefinitionList(Vec<AspaDefinition>);
|
||
|
||
impl AspaDefinitionList {
|
||
pub fn new(definitions: Vec<AspaDefinition>) -> Self {
|
||
AspaDefinitionList(definitions)
|
||
}
|
||
|
||
pub fn as_slice(&self) -> &[AspaDefinition] {
|
||
self.0.as_slice()
|
||
}
|
||
}
|
||
|
||
impl fmt::Display for AspaDefinitionList {
|
||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
for def in self.0.iter() {
|
||
writeln!(f, "{def}")?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
|
||
//------------ AspaDefinition ------------------------------------------------
|
||
|
||
/// The definition of an ASPA record.
|
||
//
|
||
// *Warning:* This type is used in stored state.
|
||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||
pub struct AspaDefinition {
|
||
/// The customer ASN.
|
||
pub customer: CustomerAsn,
|
||
|
||
/// The list of provider ASNs.
|
||
///
|
||
/// This is not necessarily an acceptable list yet, i.e., it may be
|
||
/// unordered, contain duplicates, or contain the customer ASN.
|
||
pub providers: Vec<ProviderAsn>,
|
||
}
|
||
|
||
impl AspaDefinition {
|
||
/// Returns true if the customer is used in the provider list.
|
||
///
|
||
/// This is not allowed by spec, and these definitions should
|
||
/// be rejected by Krill.
|
||
pub fn customer_used_as_provider(&self) -> bool {
|
||
self.providers.contains(&self.customer)
|
||
}
|
||
|
||
/// Returns true if there are duplicate provider ASNs.
|
||
///
|
||
/// This is not allowed by spec and these definitions should be
|
||
/// rejected by Krill.
|
||
pub fn contains_duplicate_providers(&self) -> bool {
|
||
let mut providers: Vec<Asn> = self.providers.clone();
|
||
|
||
let len_before_duplicates = providers.len();
|
||
|
||
providers.sort();
|
||
providers.dedup();
|
||
|
||
len_before_duplicates > providers.len()
|
||
}
|
||
|
||
/// Applies an update to the definition.
|
||
///
|
||
/// The update is lenient, i.e., adding exsting ASNs and removing
|
||
/// non-existing ASNs is fine. After the update, the provider ASNs are
|
||
/// sorted.
|
||
pub fn apply_update(&mut self, update: &AspaProvidersUpdate) {
|
||
for removed in &update.removed {
|
||
self.providers.retain(|provider| provider != removed);
|
||
}
|
||
|
||
for added in &update.added {
|
||
if !self.providers.contains(added) {
|
||
self.providers.push(*added);
|
||
}
|
||
}
|
||
self.providers.sort();
|
||
}
|
||
}
|
||
|
||
impl fmt::Display for AspaDefinition {
|
||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
// example: 65000 => 65001, 65002, 65003
|
||
write!(f, "{} => ", self.customer)?;
|
||
if self.providers.is_empty() {
|
||
write!(f, "<none>")?;
|
||
} else {
|
||
for i in 0..self.providers.len() {
|
||
if i > 0 {
|
||
write!(f, ", ")?;
|
||
}
|
||
write!(f, "{}", self.providers[i])?;
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl FromStr for AspaDefinition {
|
||
type Err = AspaDefinitionFormatError;
|
||
|
||
// example: 65000 => 65001, 65002(v4), 65003(v6)
|
||
// example: 65000 => <none>
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||
let mut parts = s.split("=>");
|
||
|
||
let customer = {
|
||
let customer_str = parts
|
||
.next()
|
||
.ok_or(AspaDefinitionFormatError::CustomerAsMissing)?;
|
||
CustomerAsn::from_str(customer_str.trim()).map_err(|_| {
|
||
AspaDefinitionFormatError::CustomerAsInvalid(
|
||
customer_str.trim().to_string(),
|
||
)
|
||
})?
|
||
};
|
||
|
||
let mut providers = {
|
||
let mut providers = vec![];
|
||
let providers_str = parts.next().unwrap_or("<none>");
|
||
|
||
if providers_str.trim() != "<none>" {
|
||
let provider_parts = providers_str.split(',');
|
||
for provider_part in provider_parts {
|
||
let provider = ProviderAsn::from_str(
|
||
provider_part.trim(),
|
||
)
|
||
.map_err(|_| {
|
||
AspaDefinitionFormatError::ProviderAsInvalid(
|
||
provider_part.trim().to_string(),
|
||
)
|
||
})?;
|
||
providers.push(provider);
|
||
}
|
||
}
|
||
|
||
providers
|
||
};
|
||
|
||
// unexpected extra bits are not acceptable
|
||
if parts.next().is_some() {
|
||
Err(AspaDefinitionFormatError::ExtraParts)
|
||
} else {
|
||
// Ensure that the providers are sorted, and there are no
|
||
// duplicates
|
||
providers.sort();
|
||
|
||
match providers.windows(2).find(|pair| pair[0] == pair[1]) {
|
||
Some(dup) => {
|
||
Err(AspaDefinitionFormatError::ProviderAsDuplicate(
|
||
dup[0], dup[1],
|
||
))
|
||
}
|
||
None => Ok(AspaDefinition { customer, providers }),
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
//------------ AspaProvidersUpdate -------------------------------------------
|
||
|
||
/// An update to the provider ASN list of an ASPA definition.
|
||
//
|
||
// *Warning:* This type is used in stored state.
|
||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||
pub struct AspaProvidersUpdate {
|
||
/// A list of ASNs to be added to the provider ASNs.
|
||
pub added: Vec<ProviderAsn>,
|
||
|
||
/// A list of ASNs to be removed from the provider ASNs.
|
||
pub removed: Vec<ProviderAsn>,
|
||
}
|
||
|
||
impl AspaProvidersUpdate {
|
||
/// Returns whether the update is empty.
|
||
pub fn is_empty(&self) -> bool {
|
||
self.added.is_empty() && self.removed.is_empty()
|
||
}
|
||
}
|
||
|
||
impl fmt::Display for AspaProvidersUpdate {
|
||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
if !self.added.is_empty() {
|
||
write!(f, "adding providers:")?;
|
||
for added in &self.added {
|
||
write!(f, " {added}")?;
|
||
}
|
||
write!(f, " ")?;
|
||
}
|
||
if !self.removed.is_empty() {
|
||
write!(f, "removing providers:")?;
|
||
for removed in &self.removed {
|
||
write!(f, " {removed}")?;
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
|
||
//============ Error Types ===================================================
|
||
|
||
//------------ AspaDefinitionFormatError -------------------------------------
|
||
|
||
/// An error happened while parsing an ASPA definition.
|
||
#[derive(Clone, Debug)]
|
||
pub enum AspaDefinitionFormatError {
|
||
/// The customer ASN was missing.
|
||
CustomerAsMissing,
|
||
|
||
/// The customer ASN was invalid.
|
||
CustomerAsInvalid(String),
|
||
|
||
/// The given provider ASN definition was invalid.
|
||
ProviderAsInvalid(String),
|
||
|
||
/// The given two provider ASNs are duplicates.
|
||
ProviderAsDuplicate(ProviderAsn, ProviderAsn),
|
||
|
||
/// There was trailing text in the definition.
|
||
ExtraParts,
|
||
}
|
||
|
||
impl fmt::Display for AspaDefinitionFormatError {
|
||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
write!(f, "ASPA configuration format invalid: ")?;
|
||
match self {
|
||
AspaDefinitionFormatError::CustomerAsMissing => {
|
||
write!(f, "customer AS missing")
|
||
}
|
||
AspaDefinitionFormatError::CustomerAsInvalid(s) => {
|
||
write!(f, "cannot parse customer AS: {s}")
|
||
}
|
||
AspaDefinitionFormatError::ProviderAsInvalid(s) => {
|
||
write!(f, "cannot parse provider AS: {s}")
|
||
}
|
||
AspaDefinitionFormatError::ProviderAsDuplicate(l, r) => {
|
||
write!(
|
||
f,
|
||
"duplicate AS in provider list. Found {l} and {r}"
|
||
)
|
||
}
|
||
AspaDefinitionFormatError::ExtraParts => {
|
||
write!(f, "found more than one '=>'")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl error::Error for AspaDefinitionFormatError {}
|
||
|
||
|
||
//============ Tests =========================================================
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn customer(s: &str) -> Asn {
|
||
Asn::from_str(s).unwrap()
|
||
}
|
||
|
||
fn provider(s: &str) -> ProviderAsn {
|
||
ProviderAsn::from_str(s).unwrap()
|
||
}
|
||
|
||
#[test]
|
||
fn aspa_configuration_to_from_str() {
|
||
let config = AspaDefinition {
|
||
customer: customer("AS65000"),
|
||
providers: vec![
|
||
provider("AS65001"),
|
||
provider("AS65002"),
|
||
provider("AS65003"),
|
||
],
|
||
};
|
||
let config_str = "AS65000 => AS65001, AS65002, AS65003";
|
||
|
||
let to_str = config.to_string();
|
||
assert_eq!(config_str, to_str.as_str());
|
||
|
||
let from_str = AspaDefinition::from_str(config_str).unwrap();
|
||
assert_eq!(config, from_str);
|
||
}
|
||
|
||
#[test]
|
||
fn aspa_configuration_empty_providers_from_str() {
|
||
let config = AspaDefinition {
|
||
customer: customer("AS65000"),
|
||
providers: vec![]
|
||
};
|
||
let config_str = "AS65000 => <none>";
|
||
|
||
let to_str = config.to_string();
|
||
assert_eq!(config_str, to_str.as_str());
|
||
|
||
let from_str = AspaDefinition::from_str(config_str).unwrap();
|
||
assert_eq!(config, from_str);
|
||
}
|
||
}
|
||
|