diff --git a/Cargo.lock b/Cargo.lock index c188dbe5..f3bb2396 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1257,7 +1257,7 @@ dependencies = [ [[package]] name = "rpki" version = "0.18.4" -source = "git+https://github.com/NLnetLabs/rpki-rs.git#01024d58589e55f3658885b6700d537d1ca71f38" +source = "git+https://github.com/NLnetLabs/rpki-rs.git?branch=aspa-provider-count#0403edaadf213d16994723a64c5d7af2f0a9ead3" dependencies = [ "arbitrary", "base64", diff --git a/Cargo.toml b/Cargo.toml index bbe5497e..76cdf89b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ pin-project-lite = "0.2.4" rand = "0.8.1" reqwest = { version = "0.12.4", default-features = false, features = ["blocking", "rustls-tls" ] } ring = "0.17" -rpki = { version = "0.18.3", features = [ "repository", "rrdp", "rtr", "serde", "slurm" ], git = "https://github.com/NLnetLabs/rpki-rs.git" } +rpki = { version = "0.18.3", features = [ "repository", "rrdp", "rtr", "serde", "slurm" ], git = "https://github.com/NLnetLabs/rpki-rs.git", branch = "aspa-provider-count" } rustls-pemfile = "2.1.2" serde = { version = "1.0.95", features = [ "derive" ] } serde_json = "1.0.57" diff --git a/doc/manual/source/manual-page.rst b/doc/manual/source/manual-page.rst index de1d278b..acc5d6ff 100644 --- a/doc/manual/source/manual-page.rst +++ b/doc/manual/source/manual-page.rst @@ -351,6 +351,13 @@ The available options are: If this option is present, BGPsec router keys will be processed during validation and included in the produced data set. +.. option:: --aspa-provider-limit + + Limits the number of provider ASNs allowed in an ASPA object. If more + providers are given, all ASPA assertions for the customer ASN are + dropped to avoid false rejections. The default value if not changed + via configuration or this option is 10,000 provider ASNs. + .. option:: --dirty If this option is present, unused files and directories will not be @@ -1190,6 +1197,13 @@ All values can be overridden via the command line options. included in the published dataset. If false or missing, no router keys will be included. + aspa_provider_limit + An integer value specifying the maximum number of provider ASNs + allowed in an ASPA object. If more providers are given, all ASPA + assertions for the customer ASN are dropped to avoid false + rejections. If the option is missing, a default of 10,000 + provider ASNs is used. + dirty A boolean value which, if true, specifies that unused files and directories should not be deleted from the repository directory diff --git a/src/config.rs b/src/config.rs index 3cf66637..1c7d377b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -93,6 +93,9 @@ const DEFAULT_MAX_CA_DEPTH: usize = 32; #[cfg(unix)] const DEFAULT_SYSLOG_FACILITY: Facility = Facility::LOG_DAEMON; +/// The default limit of provider ASNs in ASPA objects. +const DEFAULT_ASPA_PROVIDER_LIMIT: usize = 10_000; + //------------ Config -------------------------------------------------------- @@ -269,6 +272,12 @@ pub struct Config { /// Whether to process ASPA objects. pub enable_aspa: bool, + /// The maximum number of provider ASNs accepted in ASPA objects. + /// + /// If this number is exceeded, all ASPAs for the customer ASN of the + /// offending object are dropped. + pub aspa_provider_limit: usize, + /// Whether to not cleanup the repository directory after a validation run. /// /// If this is `false` and update has not been disabled otherwise, all @@ -622,6 +631,11 @@ impl Config { self.enable_aspa = true } + // aspa_provider_limit + if let Some(value) = args.aspa_provider_limit { + self.aspa_provider_limit = value; + } + // dirty_repository if args.dirty_repository { self.dirty_repository = true @@ -979,6 +993,11 @@ impl Config { #[cfg(not(feature = "aspa"))] enable_aspa: false, + aspa_provider_limit: { + file.take_usize("aspa-provider-limit")? + .unwrap_or(DEFAULT_ASPA_PROVIDER_LIMIT) + }, + dirty_repository: file.take_bool("dirty")?.unwrap_or(false), validation_threads: { file.take_small_usize( @@ -1188,6 +1207,7 @@ impl Config { max_ca_depth: DEFAULT_MAX_CA_DEPTH, enable_bgpsec: false, enable_aspa: false, + aspa_provider_limit: DEFAULT_ASPA_PROVIDER_LIMIT, dirty_repository: DEFAULT_DIRTY_REPOSITORY, validation_threads: Config::default_validation_threads(), refresh: Duration::from_secs(DEFAULT_REFRESH), @@ -1415,6 +1435,7 @@ impl Config { insert(&mut res, "enable-bgpsec", self.enable_bgpsec); #[cfg(feature = "aspa")] insert(&mut res, "enable-aspa", self.enable_aspa); + insert_int(&mut res, "aspa-provider-limit", self.aspa_provider_limit); insert(&mut res, "dirty", self.dirty_repository); insert_int(&mut res, "validation-threads", self.validation_threads); insert_int(&mut res, "refresh", self.refresh.as_secs()); @@ -1875,6 +1896,10 @@ struct GlobalArgs { #[arg(long)] enable_aspa: bool, + /// Maximum number of provider ASNs in ASPA objects + #[arg(long, value_name = "COUNT")] + aspa_provider_limit: Option, + /// Do not clean up repository directory after validation #[arg(long)] dirty_repository: bool, diff --git a/src/payload/validation.rs b/src/payload/validation.rs index 02781c3e..ea76c4c9 100644 --- a/src/payload/validation.rs +++ b/src/payload/validation.rs @@ -15,7 +15,7 @@ use std::cmp; use std::collections::hash_map; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use crossbeam_queue::SegQueue; use log::{info, warn}; @@ -75,6 +75,9 @@ pub struct ValidationReport { /// How are we dealing with unsafe VRPs? unsafe_vrps: FilterPolicy, + + /// Maximum number of provider ASNs on ASPA objects. + aspa_provider_limit: usize, } impl ValidationReport { @@ -89,6 +92,7 @@ impl ValidationReport { limit_v4_len: config.limit_v4_len, limit_v6_len: config.limit_v6_len, unsafe_vrps: config.unsafe_vrps, + aspa_provider_limit: config.aspa_provider_limit, } } @@ -271,7 +275,7 @@ impl ProcessPubPoint for PubPointProcessor<'_> { fn process_aspa( &mut self, - _uri: &uri::Rsync, + uri: &uri::Rsync, cert: ResourceCert, aspa: AsProviderAttestation ) -> Result<(), Failed> { @@ -279,6 +283,18 @@ impl ProcessPubPoint for PubPointProcessor<'_> { return Ok(()) } self.pub_point.update_refresh(cert.validity().not_after()); + if aspa.provider_as_set().len() > self.report.aspa_provider_limit { + warn!( + "{}: {} provider ASNs is over the limit of {}. \ + Skipping ASPA for {}.", + uri, + aspa.provider_as_set().len(), + self.report.aspa_provider_limit, + aspa.customer_as() + ); + self.report.rejected.aspa_customers.push(aspa.customer_as()); + return Ok(()) + } self.pub_point.add_aspa( aspa, Arc::new(PublishInfo::signed_object( @@ -513,6 +529,7 @@ pub struct PubAspa { pub struct RejectedResources { v4: IpBlocks, v6: IpBlocks, + aspa_customers: HashSet, } impl RejectedResources { @@ -543,6 +560,9 @@ struct RejectedResourcesBuilder { /// The queue of rejected AS blocks. asns: SegQueue, + + /// The queue of rejected ASPA customer ASNs. + aspa_customers: SegQueue, } impl RejectedResourcesBuilder { @@ -578,6 +598,7 @@ impl RejectedResourcesBuilder { RejectedResources { v4: v4.finalize(), v6: v6.finalize(), + aspa_customers: self.aspa_customers.into_iter().collect(), } } } @@ -753,6 +774,11 @@ impl<'a> SnapshotBuilder<'a> { fn process_aspa(&mut self, aspa: PubAspa, metrics: &mut AllVrpMetrics) { metrics.update(|m| m.aspas.valid += 1); + if self.rejected.aspa_customers.contains(&aspa.customer) { + metrics.update(|m| m.aspas.marked_unsafe += 1); + return + } + // SLURM filtering goes here ... match self.aspas.entry(aspa.customer) {