mirror of
https://github.com/NLnetLabs/krill.git
synced 2026-08-17 21:25:53 +02:00
Bring back the built-in RISwhois tree for BGP analysis. (#1329)
This PR brings back the built-in tree of downloaded RISwhois data rather than using the Roto API. It does so using a memory-optimized tree implementation and has a much smaller memory footprint than the previous iteration. At the time of writing, the a full RISwhois dataset requires 55 megabytes of memory. This PR also reverts the changes to the configuration. It removes the bgp_api_enabled, bgp_api_uri, and bgp_api_cache_duration fields and adds bgp_riswhois_enabled, bgp_riswhois_v4_uri, bgp_riswhois_v6_uri, and bgp_riswhois_refresh_duration fields, all of which are optional. Because of these config changes, the PR is a breaking change.
This commit is contained in:
Generated
+364
-391
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ exclude = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
arc-swap = "1.7.1"
|
||||
base64 = "0.22.1"
|
||||
bytes = "1"
|
||||
chrono = { version = "0.4.39", features = ["serde"] }
|
||||
@@ -34,6 +35,7 @@ hyper = { version = "1.6.0", features = ["server"] }
|
||||
hyper-util = { version = "0.1", features = [ "server" ] }
|
||||
intervaltree = "0.2.7"
|
||||
lazy_static = "1.5"
|
||||
libflate = "2.1.0"
|
||||
log = "0.4"
|
||||
openssl = { version = "0.10", features = ["v110"] }
|
||||
percent-encoding = "2.3.1"
|
||||
|
||||
+5
-4
@@ -352,11 +352,12 @@
|
||||
#
|
||||
### ca_refresh_jitter_seconds = 43200
|
||||
|
||||
# Enable loading information from bgp-api.net for ROA vs BGP analysis.
|
||||
# Enable downloading RISwhois data for ROA vs BGP analysis.
|
||||
#
|
||||
### bgp_api_enabled = true
|
||||
### bgp_api_uri = "https://rest.bgp-api.net"
|
||||
### bgp_api_cache_seconds = 1800
|
||||
### bgp_riswhois_enabled = true
|
||||
### bgp_riswhois_v4_uri = "https://www.ris.ripe.net/dumps/riswhoisdump.IPv4.gz"
|
||||
### bgp_riswhois_v6_uri = "https://www.ris.ripe.net/dumps/riswhoisdump.IPv6.gz"
|
||||
### bgp_riswhois_refresh_minutes = 60
|
||||
|
||||
# Restrict size of messages sent to the API.
|
||||
#
|
||||
|
||||
+1
-2
@@ -446,11 +446,10 @@ impl fmt::Display for BgpAnalysisReport {
|
||||
writeln!(f)?;
|
||||
writeln!(
|
||||
f,
|
||||
"\ttConfiguration: {}",
|
||||
"\tConfiguration: {}",
|
||||
roa.configured_roa()
|
||||
)?;
|
||||
writeln!(f)?;
|
||||
writeln!(f)?;
|
||||
writeln!(f, "\t\tDisallows:")?;
|
||||
for ann in roa.disallows.iter() {
|
||||
writeln!(f, "\t\t{ann}")?;
|
||||
|
||||
+181
-28
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::{error, fmt};
|
||||
use std::cmp::Ordering;
|
||||
use std::net::IpAddr;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use std::str::FromStr;
|
||||
use rpki::uri;
|
||||
use rpki::ca::publication::Base64;
|
||||
@@ -661,16 +661,19 @@ impl TypedPrefix {
|
||||
}
|
||||
|
||||
/// Returns the IP address part of the prefix.
|
||||
pub fn ip_addr(&self) -> IpAddr {
|
||||
pub fn ip_addr(self) -> IpAddr {
|
||||
match self {
|
||||
Self::V4(v4) => IpAddr::V4(v4.0.to_v4()),
|
||||
Self::V6(v6) => IpAddr::V6(v6.0.to_v6()),
|
||||
Self::V4(v4) => v4.addr().into(),
|
||||
Self::V6(v6) => v6.addr().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the prefix length of the prefix.
|
||||
pub fn addr_len(&self) -> u8 {
|
||||
self.prefix().addr_len()
|
||||
pub fn addr_len(self) -> u8 {
|
||||
match self {
|
||||
Self::V4(v4) => v4.addr_len(),
|
||||
Self::V6(v6) => v6.addr_len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether `other` is of the same address family.
|
||||
@@ -715,7 +718,7 @@ impl From<TypedPrefix> for ResourceSet {
|
||||
match tp {
|
||||
TypedPrefix::V4(v4) => {
|
||||
let mut builder = IpBlocksBuilder::new();
|
||||
builder.push(v4.0);
|
||||
builder.push(Prefix::from(v4));
|
||||
let blocks = builder.finalize();
|
||||
|
||||
ResourceSet::new(
|
||||
@@ -726,7 +729,7 @@ impl From<TypedPrefix> for ResourceSet {
|
||||
}
|
||||
TypedPrefix::V6(v6) => {
|
||||
let mut builder = IpBlocksBuilder::new();
|
||||
builder.push(v6.0);
|
||||
builder.push(Prefix::from(v6));
|
||||
let blocks = builder.finalize();
|
||||
|
||||
ResourceSet::new(
|
||||
@@ -744,12 +747,12 @@ impl FromStr for TypedPrefix {
|
||||
|
||||
fn from_str(prefix: &str) -> Result<Self, Self::Err> {
|
||||
if prefix.contains('.') {
|
||||
Ok(TypedPrefix::V4(Ipv4Prefix(
|
||||
Ok(TypedPrefix::V4(Ipv4Prefix::from(
|
||||
Prefix::from_v4_str(prefix.trim())
|
||||
.map_err(|_| AuthorizationFmtError::pfx(prefix))?,
|
||||
)))
|
||||
} else {
|
||||
Ok(TypedPrefix::V6(Ipv6Prefix(
|
||||
Ok(TypedPrefix::V6(Ipv6Prefix::from(
|
||||
Prefix::from_v6_str(prefix.trim())
|
||||
.map_err(|_| AuthorizationFmtError::pfx(prefix))?,
|
||||
)))
|
||||
@@ -822,18 +825,82 @@ impl Serialize for TypedPrefix {
|
||||
/// An IPv4 prefix.
|
||||
//
|
||||
// *Warning:* This type is used in stored state.
|
||||
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub struct Ipv4Prefix(Prefix);
|
||||
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct Ipv4Prefix {
|
||||
/// The address portion of the prefix.
|
||||
///
|
||||
/// This cannot be pub because we need to enforce that non-prefix bits are
|
||||
/// zero.
|
||||
addr: Ipv4Addr,
|
||||
|
||||
impl AsRef<Prefix> for Ipv4Prefix {
|
||||
fn as_ref(&self) -> &Prefix {
|
||||
&self.0
|
||||
/// The address length.
|
||||
///
|
||||
/// This cannot be pub because it needs to be less than 33.
|
||||
addr_len: u8,
|
||||
}
|
||||
|
||||
impl Ipv4Prefix {
|
||||
/// Returns the address portion of the prefix.
|
||||
pub fn addr(self) -> Ipv4Addr {
|
||||
self.addr
|
||||
}
|
||||
|
||||
/// Returns the address length.
|
||||
pub fn addr_len(self) -> u8 {
|
||||
self.addr_len
|
||||
}
|
||||
|
||||
/// Returns a prefix with the same address but given length.
|
||||
pub fn resize(self, addr_len: u8) -> Self {
|
||||
if addr_len >= 32 {
|
||||
Self {
|
||||
addr: self.addr,
|
||||
addr_len: 32,
|
||||
}
|
||||
}
|
||||
else {
|
||||
Self {
|
||||
addr: Ipv4Addr::from_bits(
|
||||
self.addr.to_bits() & !(u32::MAX >> addr_len)
|
||||
),
|
||||
addr_len
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Ipv4Prefix {
|
||||
fn default() -> Self {
|
||||
Self { addr: Ipv4Addr::UNSPECIFIED, addr_len: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Ipv4Prefix {
|
||||
type Err = ParsePrefixError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let Some((addr, len)) = s.split_once('/') else {
|
||||
return Err(ParsePrefixError(()))
|
||||
};
|
||||
let addr = Ipv4Addr::from_str(addr).map_err(|_| {
|
||||
ParsePrefixError(())
|
||||
})?;
|
||||
let addr_len = u8::from_str(len).map_err(|_| {
|
||||
ParsePrefixError(())
|
||||
})?;
|
||||
if addr_len > 32 {
|
||||
return Err(ParsePrefixError(()));
|
||||
}
|
||||
if addr.to_bits().trailing_zeros() < (32 - addr_len).into() {
|
||||
return Err(ParsePrefixError(()));
|
||||
}
|
||||
Ok(Self { addr, addr_len })
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Ipv4Prefix {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}/{}", self.0.to_v4(), self.0.addr_len())
|
||||
write!(f, "{}/{}", self.addr, self.addr_len)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -845,13 +912,16 @@ impl fmt::Debug for Ipv4Prefix {
|
||||
|
||||
impl From<Prefix> for Ipv4Prefix {
|
||||
fn from(prefix: Prefix) -> Self {
|
||||
Ipv4Prefix(prefix)
|
||||
Self {
|
||||
addr: prefix.to_v4(),
|
||||
addr_len: prefix.addr_len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Ipv4Prefix> for Prefix {
|
||||
fn from(prefix: Ipv4Prefix) -> Self {
|
||||
prefix.0
|
||||
fn from(src: Ipv4Prefix) -> Self {
|
||||
Self::new(src.addr, src.addr_len)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -860,18 +930,82 @@ impl From<Ipv4Prefix> for Prefix {
|
||||
/// An IPv6 prefix.
|
||||
//
|
||||
// *Warning:* This type is used in stored state.
|
||||
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub struct Ipv6Prefix(Prefix);
|
||||
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct Ipv6Prefix {
|
||||
/// The address portion of the prefix.
|
||||
///
|
||||
/// This cannot be pub because we need to enforce that non-prefix bits
|
||||
/// are zero.
|
||||
addr: Ipv6Addr,
|
||||
|
||||
impl AsRef<Prefix> for Ipv6Prefix {
|
||||
fn as_ref(&self) -> &Prefix {
|
||||
&self.0
|
||||
/// The address length.
|
||||
///
|
||||
/// This cannot be pub because it needs to be less than 129.
|
||||
addr_len: u8,
|
||||
}
|
||||
|
||||
impl Ipv6Prefix {
|
||||
/// Returns the address portion of the prefix.
|
||||
pub fn addr(self) -> Ipv6Addr {
|
||||
self.addr
|
||||
}
|
||||
|
||||
/// Returns the address length.
|
||||
pub fn addr_len(self) -> u8 {
|
||||
self.addr_len
|
||||
}
|
||||
|
||||
/// Returns a prefix with the same address but given length.
|
||||
pub fn resize(self, addr_len: u8) -> Self {
|
||||
if addr_len >= 128 {
|
||||
Self {
|
||||
addr: self.addr,
|
||||
addr_len: 128,
|
||||
}
|
||||
}
|
||||
else {
|
||||
Self {
|
||||
addr: Ipv6Addr::from_bits(
|
||||
self.addr.to_bits() & !(u128::MAX >> addr_len)
|
||||
),
|
||||
addr_len
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Ipv6Prefix {
|
||||
fn default() -> Self {
|
||||
Self { addr: Ipv6Addr::UNSPECIFIED, addr_len: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Ipv6Prefix {
|
||||
type Err = ParsePrefixError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let Some((addr, len)) = s.split_once('/') else {
|
||||
return Err(ParsePrefixError(()))
|
||||
};
|
||||
let addr = Ipv6Addr::from_str(addr).map_err(|_| {
|
||||
ParsePrefixError(())
|
||||
})?;
|
||||
let addr_len = u8::from_str(len).map_err(|_| {
|
||||
ParsePrefixError(())
|
||||
})?;
|
||||
if addr_len > 128 {
|
||||
return Err(ParsePrefixError(()));
|
||||
}
|
||||
if addr.to_bits().trailing_zeros() < (128 - addr_len).into() {
|
||||
return Err(ParsePrefixError(()));
|
||||
}
|
||||
Ok(Self { addr, addr_len })
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Ipv6Prefix {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}/{}", self.0.to_v6(), self.0.addr_len())
|
||||
write!(f, "{}/{}", self.addr, self.addr_len)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -883,13 +1017,16 @@ impl fmt::Debug for Ipv6Prefix {
|
||||
|
||||
impl From<Prefix> for Ipv6Prefix {
|
||||
fn from(prefix: Prefix) -> Self {
|
||||
Ipv6Prefix(prefix)
|
||||
Self {
|
||||
addr: prefix.to_v6(),
|
||||
addr_len: prefix.addr_len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Ipv6Prefix> for Prefix {
|
||||
fn from(prefix: Ipv6Prefix) -> Self {
|
||||
prefix.0
|
||||
fn from(src: Ipv6Prefix) -> Self {
|
||||
Self::new(src.addr, src.addr_len)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1009,6 +1146,22 @@ impl fmt::Display for AuthorizationFmtError {
|
||||
impl error::Error for AuthorizationFmtError { }
|
||||
|
||||
|
||||
//------------ ParsePrefixError ----------------------------------------------
|
||||
|
||||
/// An error happened while parsing a prefix.
|
||||
#[derive(Debug)]
|
||||
pub struct ParsePrefixError(());
|
||||
|
||||
impl fmt::Display for ParsePrefixError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("invalid prefix")
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for ParsePrefixError { }
|
||||
|
||||
|
||||
|
||||
//============ Tests =========================================================
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+34
-22
@@ -177,16 +177,20 @@ impl ConfigDefaults {
|
||||
240 // 4 minutes by default should be plenty in most cases
|
||||
}
|
||||
|
||||
pub fn bgp_api_enabled() -> bool {
|
||||
pub fn bgp_riswhois_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn bgp_api_uri() -> String {
|
||||
"https://rest.bgp-api.net".to_string()
|
||||
pub fn bgp_riswhois_v4_uri() -> String {
|
||||
"https://www.ris.ripe.net/dumps/riswhoisdump.IPv4.gz".into()
|
||||
}
|
||||
|
||||
pub fn bgp_api_cache_duration() -> Duration {
|
||||
Duration::seconds(30 * 60)
|
||||
pub fn bgp_riswhois_v6_uri() -> String {
|
||||
"https://www.ris.ripe.net/dumps/riswhoisdump.IPv6.gz".into()
|
||||
}
|
||||
|
||||
pub fn bgp_riswhois_refresh_interval() -> Duration {
|
||||
Duration::seconds(60 * 60)
|
||||
}
|
||||
|
||||
pub fn roa_aggregate_threshold() -> usize {
|
||||
@@ -461,10 +465,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_seconds_duration<'de, D: Deserializer<'de>>(
|
||||
fn deserialize_minutes_duration<'de, D: Deserializer<'de>>(
|
||||
deserializer: D
|
||||
) -> Result<Duration, D::Error> {
|
||||
u32::deserialize(deserializer).map(|secs| Duration::seconds(secs.into()))
|
||||
u32::deserialize(deserializer).map(|secs| {
|
||||
Duration::seconds(i64::from(secs) * 60)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -596,19 +602,22 @@ pub struct Config {
|
||||
#[serde(default = "ConfigDefaults::rfc6492_log_dir")]
|
||||
pub rfc6492_log_dir: Option<PathBuf>,
|
||||
|
||||
// RIS BGP
|
||||
#[serde(default = "ConfigDefaults::bgp_api_enabled")]
|
||||
pub bgp_api_enabled: bool,
|
||||
// RISwhois data for the BGP analyser
|
||||
#[serde(default = "ConfigDefaults::bgp_riswhois_enabled")]
|
||||
pub bgp_riswhois_enabled: bool,
|
||||
|
||||
#[serde(default = "ConfigDefaults::bgp_api_uri")]
|
||||
pub bgp_api_uri: String,
|
||||
#[serde(default = "ConfigDefaults::bgp_riswhois_v4_uri")]
|
||||
pub bgp_riswhois_v4_uri: String,
|
||||
|
||||
#[serde(default = "ConfigDefaults::bgp_riswhois_v6_uri")]
|
||||
pub bgp_riswhois_v6_uri: String,
|
||||
|
||||
#[serde(
|
||||
rename = "bgp_api_cache_seconds",
|
||||
default = "ConfigDefaults::bgp_api_cache_duration",
|
||||
deserialize_with = "deserialize_seconds_duration",
|
||||
rename = "bgp_riswhois_refresh_minutes",
|
||||
default = "ConfigDefaults::bgp_riswhois_refresh_interval",
|
||||
deserialize_with = "deserialize_minutes_duration",
|
||||
)]
|
||||
pub bgp_api_cache_duration: Duration,
|
||||
pub bgp_riswhois_refresh_interval: Duration,
|
||||
|
||||
// ROA Aggregation per ASN
|
||||
#[serde(default = "ConfigDefaults::roa_aggregate_threshold")]
|
||||
@@ -1140,9 +1149,11 @@ impl Config {
|
||||
let post_protocol_msg_timeout_seconds =
|
||||
ConfigDefaults::post_protocol_msg_timeout_seconds();
|
||||
|
||||
let bgp_api_enabled = false;
|
||||
let bgp_api_uri = ConfigDefaults::bgp_api_uri();
|
||||
let bgp_api_cache_duration = ConfigDefaults::bgp_api_cache_duration();
|
||||
let bgp_riswhois_enabled = false;
|
||||
let bgp_riswhois_v4_uri = ConfigDefaults::bgp_riswhois_v4_uri();
|
||||
let bgp_riswhois_v6_uri = ConfigDefaults::bgp_riswhois_v6_uri();
|
||||
let bgp_riswhois_refresh_interval
|
||||
= ConfigDefaults::bgp_riswhois_refresh_interval();
|
||||
|
||||
let roa_aggregate_threshold = 3;
|
||||
let roa_deaggregate_threshold = 2;
|
||||
@@ -1252,9 +1263,10 @@ impl Config {
|
||||
post_limit_rfc6492,
|
||||
rfc6492_log_dir: None,
|
||||
post_protocol_msg_timeout_seconds,
|
||||
bgp_api_enabled,
|
||||
bgp_api_uri,
|
||||
bgp_api_cache_duration,
|
||||
bgp_riswhois_enabled,
|
||||
bgp_riswhois_v4_uri,
|
||||
bgp_riswhois_v6_uri,
|
||||
bgp_riswhois_refresh_interval,
|
||||
roa_aggregate_threshold,
|
||||
roa_deaggregate_threshold,
|
||||
issuance_timing,
|
||||
|
||||
@@ -902,13 +902,13 @@ async fn routes_try(
|
||||
= request.read_json::<RoaConfigurationUpdates>().await?;
|
||||
let effect = server.krill().ca_routes_bgp_dry_run(
|
||||
&ca, updates.clone()
|
||||
).await?;
|
||||
)?;
|
||||
if effect.contains_invalids() {
|
||||
updates.set_explicit_max_length();
|
||||
let resources = updates.affected_prefixes();
|
||||
let suggestion = server.krill().ca_routes_bgp_suggest(
|
||||
&ca, Some(resources)
|
||||
).await?;
|
||||
)?;
|
||||
Ok(HttpResponse::json(
|
||||
&BgpAnalysisAdvice {
|
||||
effect, suggestion,
|
||||
@@ -927,14 +927,14 @@ async fn routes_analysis(
|
||||
ca: CaHandle,
|
||||
) -> Result<HttpResponse, DispatchError> {
|
||||
match path.next() {
|
||||
Some("full") => routes_analysis_full(request, path, ca).await,
|
||||
Some("full") => routes_analysis_full(request, path, ca),
|
||||
Some("dryrun") => routes_analysis_dryrun(request, path, ca).await,
|
||||
Some("suggest") => routes_analysis_suggest(request, path, ca).await,
|
||||
_ => Ok(HttpResponse::not_found())
|
||||
}
|
||||
}
|
||||
|
||||
async fn routes_analysis_full(
|
||||
fn routes_analysis_full(
|
||||
request: Request<'_>,
|
||||
path: PathIter<'_>,
|
||||
ca: CaHandle,
|
||||
@@ -946,7 +946,7 @@ async fn routes_analysis_full(
|
||||
)?;
|
||||
let server = request.empty()?;
|
||||
Ok(HttpResponse::json(
|
||||
&server.krill().ca_routes_bgp_analysis(&ca).await?
|
||||
&server.krill().ca_routes_bgp_analysis(&ca)?
|
||||
))
|
||||
}
|
||||
|
||||
@@ -962,7 +962,7 @@ async fn routes_analysis_dryrun(
|
||||
)?;
|
||||
let (server, updates) = request.read_json().await?;
|
||||
Ok(HttpResponse::json(
|
||||
&server.krill().ca_routes_bgp_dry_run(&ca, updates).await?
|
||||
&server.krill().ca_routes_bgp_dry_run(&ca, updates)?
|
||||
))
|
||||
}
|
||||
|
||||
@@ -979,7 +979,7 @@ async fn routes_analysis_suggest(
|
||||
)?;
|
||||
let server = request.empty()?;
|
||||
Ok(HttpResponse::json(
|
||||
&server.krill().ca_routes_bgp_suggest(&ca, None).await?
|
||||
&server.krill().ca_routes_bgp_suggest(&ca, None)?
|
||||
))
|
||||
}
|
||||
Method::POST => {
|
||||
@@ -990,7 +990,7 @@ async fn routes_analysis_suggest(
|
||||
Ok(HttpResponse::json(
|
||||
&server.krill().ca_routes_bgp_suggest(
|
||||
&ca, Some(resources)
|
||||
).await?
|
||||
)?
|
||||
))
|
||||
}
|
||||
_ => Ok(HttpResponse::method_not_allowed())
|
||||
|
||||
@@ -59,7 +59,7 @@ pub async fn dispatch(
|
||||
server.authorizer().login_session_cache_size().await,
|
||||
);
|
||||
|
||||
if let Ok(cas_stats) = server.krill().cas_stats().await {
|
||||
if let Ok(cas_stats) = server.krill().cas_stats() {
|
||||
target.single(
|
||||
Metric::gauge("cas", "number of CAs in Krill"),
|
||||
cas_stats.len()
|
||||
|
||||
@@ -58,6 +58,6 @@ async fn cas(
|
||||
request.check_get()?;
|
||||
let (request, _) = request.proceed_unchecked();
|
||||
let server = request.empty()?;
|
||||
Ok(HttpResponse::json(&server.krill().cas_stats().await?))
|
||||
Ok(HttpResponse::json(&server.krill().cas_stats()?))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,998 @@
|
||||
//! The actual BGP analyser and its supporting, private data structures.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicI64, Ordering};
|
||||
use arc_swap::ArcSwapOption;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use log::trace;
|
||||
use rpki::repository::resources::{IpBlock, ResourceSet};
|
||||
use rpki::repository::x509::Time;
|
||||
use crate::api::bgp::{
|
||||
Announcement, BgpAnalysisEntry, BgpAnalysisReport, BgpAnalysisState,
|
||||
BgpAnalysisSuggestion, ReplacementRoaSuggestion,
|
||||
};
|
||||
use crate::api::roa::{
|
||||
AsNumber, ConfiguredRoa, Ipv4Prefix, Ipv6Prefix, RoaPayload, TypedPrefix,
|
||||
};
|
||||
use crate::config::Config;
|
||||
use super::riswhois::{
|
||||
RisWhois, RisWhoisError, RisWhoisLoader, RouteOrigin, RouteOriginSet,
|
||||
RoutePrefix,
|
||||
};
|
||||
|
||||
|
||||
//------------ BgpAnalyser -------------------------------------------------
|
||||
|
||||
/// An analyser for the effects of ROAs against real-world BGP data.
|
||||
///
|
||||
/// The analyser is configured with the URLs of the RISwhois dumps and
|
||||
/// whether to download them at all and if so, how often. It doesn’t download
|
||||
/// data immediately but only when the [`update`][Self::update] method is
|
||||
/// called.
|
||||
///
|
||||
/// There are two methods that perform an analyis: [`analyse`][Self::analyse]
|
||||
/// produces a report for an existing set of ROAs while
|
||||
/// [`suggest`][Self::suggest] also adds suggestions what ROAs should be
|
||||
/// created.
|
||||
pub struct BgpAnalyser {
|
||||
/// The loader for RISwhois data.
|
||||
///
|
||||
/// If this is `None`, loading data has been disabled.
|
||||
loader: Option<RisWhoisLoader>,
|
||||
|
||||
/// How long should we wait before downloading the data again.
|
||||
refresh_interval: Duration,
|
||||
|
||||
/// The last time we downloaded the data.
|
||||
///
|
||||
/// This is the Unix timestamp in full seconds of that time. If we never
|
||||
/// downloaded the data, this will be set to `i64::MIN`.
|
||||
last_checked: AtomicI64,
|
||||
|
||||
/// The current set of RISwhois data.
|
||||
///
|
||||
/// This may be `None` if we haven’t downloaded a set (yet).
|
||||
riswhois: ArcSwapOption<RisWhois>,
|
||||
}
|
||||
|
||||
impl BgpAnalyser {
|
||||
/// Creates a new analyser using information in the config.
|
||||
pub fn new(config: &Config) -> Self {
|
||||
Self {
|
||||
loader: config.bgp_riswhois_enabled.then(|| {
|
||||
RisWhoisLoader::new(
|
||||
config.bgp_riswhois_v4_uri.clone(),
|
||||
config.bgp_riswhois_v6_uri.clone(),
|
||||
)
|
||||
}),
|
||||
refresh_interval: config.bgp_riswhois_refresh_interval,
|
||||
last_checked: i64::MIN.into(),
|
||||
riswhois: ArcSwapOption::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the RISwhois dataset.
|
||||
///
|
||||
/// This can be called at any time and will only actually download data
|
||||
/// if downloading has been enabled and if the configured refresh
|
||||
/// duration has passed since the last download.
|
||||
///
|
||||
/// Returns `Ok(true)` if it did do a download, `Ok(false)` if no download
|
||||
/// was necessary, or an error if downloading was attempted but failed.
|
||||
pub async fn update(&self) -> Result<bool, RisWhoisError> {
|
||||
let Some(loader) = self.loader.as_ref() else {
|
||||
return Ok(false)
|
||||
};
|
||||
let last_checked = Time::new(
|
||||
DateTime::from_timestamp(
|
||||
self.last_checked.load(Ordering::Relaxed), 0
|
||||
).unwrap_or(DateTime::<Utc>::MIN_UTC)
|
||||
);
|
||||
if last_checked + self.refresh_interval >= Time::now() {
|
||||
trace!(
|
||||
"RISwhois update requested but refresh duration \
|
||||
has not yet passed."
|
||||
);
|
||||
return Ok(false)
|
||||
}
|
||||
|
||||
self.riswhois.store(Some(Arc::new(loader.load().await?)));
|
||||
self.last_checked.store(Time::now().timestamp(), Ordering::Relaxed);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Creates a BGP analysis report for a set of ROAs and resources.
|
||||
///
|
||||
/// The ROAs to be analysed are given via `roas` and the resources held
|
||||
/// by the CA publishing the ROAs via `resources_held`. If required,
|
||||
/// the ROAs to be analysed can be limited to those covered by the
|
||||
/// resource set given through `limited_scope`.
|
||||
///
|
||||
/// The method returns a BGP analysis report providing information on
|
||||
/// how the ROAs and resources based on the current RISwhois data. If no
|
||||
/// data is currently available, the report will contain “no announcement
|
||||
/// info” for each ROA.
|
||||
pub fn analyse(
|
||||
&self,
|
||||
roas: &[ConfiguredRoa],
|
||||
resources_held: &ResourceSet,
|
||||
limited_scope: Option<ResourceSet>,
|
||||
) -> BgpAnalysisReport {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
// Create a list of the ROAs that are contained in the held
|
||||
// resources but not in the limited scope. Everything that is in
|
||||
// neither goes directly into the `entries` as ‘not held.’
|
||||
let mut roas_held = Vec::new();
|
||||
for roa in roas {
|
||||
if let Some(limit) = limited_scope.as_ref() {
|
||||
if !limit.contains_roa_address(
|
||||
&roa.roa_configuration.payload.as_roa_ip_address()
|
||||
) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if resources_held.contains_roa_address(
|
||||
&roa.roa_configuration.payload.as_roa_ip_address()
|
||||
) {
|
||||
roas_held.push(roa.clone());
|
||||
}
|
||||
else {
|
||||
entries.push(BgpAnalysisEntry::roa_not_held(roa.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// If we don’t have RISwhois data, we can add the held ROAs as well
|
||||
// and return.
|
||||
let seen = self.riswhois.load();
|
||||
let Some(seen) = seen.as_ref() else {
|
||||
for roa in roas_held {
|
||||
entries.push(BgpAnalysisEntry::roa_no_announcement_info(roa));
|
||||
}
|
||||
return BgpAnalysisReport::new(entries)
|
||||
};
|
||||
|
||||
// Determine the scope of our analysis. This is `limited_scope` if
|
||||
// present or the held resources otherwise.
|
||||
let scope = limited_scope.as_ref().unwrap_or(resources_held);
|
||||
|
||||
// Convert the scope to a list of prefixes for v4 and v6 each.
|
||||
let (v4_scope, v6_scope) = Self::get_prefixes_from_scope(scope);
|
||||
|
||||
// The original code now collects all route origins seen under these
|
||||
// prefixes into `scoped_announcements`. We don’t really need that
|
||||
// since we can just walk our trees if necessary.
|
||||
|
||||
// Extract the ROA prefixes and break them up into v4 and v6.
|
||||
let (v4_roas, v6_roas) = Self::split_roas(&roas_held);
|
||||
|
||||
// Next, go over all route origins in scope and validate them using
|
||||
// the held ROAs.
|
||||
let mut v4_validated = Vec::new();
|
||||
for v4 in v4_scope {
|
||||
for route_origins in seen.v4().eq_or_more_specific(v4) {
|
||||
ValidatedRouteOrigin::validate_set(
|
||||
route_origins, &v4_roas, &mut v4_validated,
|
||||
)
|
||||
}
|
||||
}
|
||||
let mut v6_validated = Vec::new();
|
||||
for v6 in v6_scope {
|
||||
for route_origins in seen.v6().eq_or_more_specific(v6) {
|
||||
ValidatedRouteOrigin::validate_set(
|
||||
route_origins, &v6_roas, &mut v6_validated,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, go over all ROAs and determine their state based on the
|
||||
// validated route_origins.
|
||||
for roa in &v4_roas {
|
||||
entries.push(
|
||||
Self::categorise_roa(
|
||||
*roa, &v4_validated, &v4_roas,
|
||||
)
|
||||
);
|
||||
}
|
||||
for roa in &v6_roas {
|
||||
entries.push(
|
||||
Self::categorise_roa(
|
||||
*roa, &v6_validated, &v6_roas,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Add the status of all route origins.
|
||||
entries.extend(v4_validated.into_iter().map(|origin| {
|
||||
origin.into_analysis_entry()
|
||||
}));
|
||||
entries.extend(v6_validated.into_iter().map(|origin| {
|
||||
origin.into_analysis_entry()
|
||||
}));
|
||||
|
||||
BgpAnalysisReport::new(entries)
|
||||
}
|
||||
|
||||
/// Create a BGP suggestions report for a set of ROAs and resources.
|
||||
///
|
||||
/// This is very similar to the [`analyse`][Self::analyse] method but
|
||||
/// the returned report also contains suggestions what ROAs the CA should
|
||||
/// contain.
|
||||
pub fn suggest(
|
||||
&self,
|
||||
roas: &[ConfiguredRoa],
|
||||
resources_held: &ResourceSet,
|
||||
limited_scope: Option<ResourceSet>,
|
||||
) -> BgpAnalysisSuggestion {
|
||||
let mut suggestion = BgpAnalysisSuggestion::default();
|
||||
|
||||
// perform analysis
|
||||
let entries = self.analyse(
|
||||
roas, resources_held, limited_scope
|
||||
).into_entries();
|
||||
for entry in &entries {
|
||||
match entry.state() {
|
||||
BgpAnalysisState::RoaUnseen => {
|
||||
suggestion.stale.push(entry.configured_roa().clone())
|
||||
}
|
||||
BgpAnalysisState::RoaTooPermissive => {
|
||||
let replace_with = entry
|
||||
.authorizes()
|
||||
.iter()
|
||||
.filter(|ann| {
|
||||
!entries.iter().any(|other| {
|
||||
other != entry
|
||||
&& other.authorizes().contains(*ann)
|
||||
})
|
||||
})
|
||||
.map(|auth| RoaPayload::from(*auth))
|
||||
.collect();
|
||||
|
||||
suggestion.too_permissive.push(
|
||||
ReplacementRoaSuggestion {
|
||||
current: entry.configured_roa().clone(),
|
||||
new: replace_with,
|
||||
}
|
||||
);
|
||||
}
|
||||
BgpAnalysisState::RoaSeen | BgpAnalysisState::RoaAs0 => {
|
||||
suggestion.keep.push(entry.configured_roa().clone())
|
||||
}
|
||||
BgpAnalysisState::RoaDisallowing => {
|
||||
suggestion.disallowing.push(entry.configured_roa().clone())
|
||||
}
|
||||
BgpAnalysisState::RoaRedundant => {
|
||||
suggestion.redundant.push(entry.configured_roa().clone())
|
||||
}
|
||||
BgpAnalysisState::RoaNotHeld => {
|
||||
suggestion.not_held.push(entry.configured_roa().clone())
|
||||
}
|
||||
BgpAnalysisState::RoaAs0Redundant => {
|
||||
suggestion.as0_redundant.push(
|
||||
entry.configured_roa().clone()
|
||||
)
|
||||
}
|
||||
BgpAnalysisState::AnnouncementValid => {}
|
||||
BgpAnalysisState::AnnouncementNotFound => {
|
||||
suggestion.not_found.push(entry.announcement())
|
||||
}
|
||||
BgpAnalysisState::AnnouncementInvalidAsn => {
|
||||
suggestion.invalid_asn.push(entry.announcement())
|
||||
}
|
||||
BgpAnalysisState::AnnouncementInvalidLength => {
|
||||
suggestion.invalid_length.push(entry.announcement())
|
||||
}
|
||||
BgpAnalysisState::AnnouncementDisallowed => {
|
||||
suggestion.keep_disallowing.push(entry.announcement())
|
||||
}
|
||||
BgpAnalysisState::RoaNoAnnouncementInfo => {
|
||||
suggestion.keep.push(entry.configured_roa().clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suggestion
|
||||
}
|
||||
|
||||
/// Returns the address prefixes contained in a resource set.
|
||||
///
|
||||
/// The function will return two vecs, one for IPv4 and one for IPv6
|
||||
/// prefixes.
|
||||
fn get_prefixes_from_scope(
|
||||
scope: &ResourceSet
|
||||
) -> (Vec<Ipv4Prefix>, Vec<Ipv6Prefix>) {
|
||||
let mut v4 = Vec::new();
|
||||
for block in scope.ipv4().iter() {
|
||||
match block {
|
||||
IpBlock::Prefix(prefix) => v4.push(Ipv4Prefix::from(prefix)),
|
||||
IpBlock::Range(range) => {
|
||||
v4.extend(range.to_v4_prefixes().map(Ipv4Prefix::from))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut v6 = Vec::new();
|
||||
for block in scope.ipv6().iter() {
|
||||
match block {
|
||||
IpBlock::Prefix(prefix) => v6.push(Ipv6Prefix::from(prefix)),
|
||||
IpBlock::Range(range) => {
|
||||
v6.extend(range.to_v6_prefixes().map(Ipv6Prefix::from))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// XXX This chould drop prefixes that are covered by other prefixes.
|
||||
// Allthough, in practice, this shouldn’t happen, since the
|
||||
// relevant RFC doesn’t allow resource sets to be like that.
|
||||
|
||||
(v4, v6)
|
||||
}
|
||||
|
||||
/// Splits a set of ROAs into those for IPv4 and IPv6.
|
||||
///
|
||||
/// The function wraps each ROA into a type that also contains the
|
||||
/// address prefix of the ROA and a reference to the [`ConfiguredRoa`].
|
||||
fn split_roas(
|
||||
roas: &[ConfiguredRoa]
|
||||
) -> (Vec<Roa<'_, Ipv4Prefix>>, Vec<Roa<'_, Ipv6Prefix>>) {
|
||||
let mut v4 = Vec::new();
|
||||
let mut v6 = Vec::new();
|
||||
for roa in roas {
|
||||
match roa.roa_configuration.payload.prefix {
|
||||
TypedPrefix::V4(prefix) => v4.push(Roa::new(prefix, roa)),
|
||||
TypedPrefix::V6(prefix) => v6.push(Roa::new( prefix, roa)),
|
||||
}
|
||||
}
|
||||
(v4, v6)
|
||||
}
|
||||
|
||||
/// Categorises a ROA for analysis.
|
||||
///
|
||||
/// The function takes a roa, a set of validated route origins, and the
|
||||
/// set of all ROAs and translates it into a [`BgpAnalysisEntry`] for the
|
||||
/// report.
|
||||
fn categorise_roa<P: RoutePrefix>(
|
||||
roa: Roa<P>,
|
||||
validated_origins: &[ValidatedRouteOrigin<P>],
|
||||
all_roas: &[Roa<P>],
|
||||
) -> BgpAnalysisEntry {
|
||||
// Get all validated origins covered by the prefix.
|
||||
let covered = validated_origins.iter().filter(|origin| {
|
||||
roa.prefix.covers(origin.route_origin.prefix)
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
// Find other ROAs that cover this ROA. Their max-len may be less,
|
||||
// so they don’t make this announcement superfluous.
|
||||
let other_roas_covering_this_prefix = all_roas.iter().filter(|other| {
|
||||
other.prefix.covers(roa.prefix) && roa.payload() != other.payload()
|
||||
}).map(|roa| roa.payload()).collect::<Vec<_>>();
|
||||
|
||||
// Find other ROAs that include this ROA’s definition and thus make
|
||||
// it superfluous.
|
||||
let other_roas_including_this_definition
|
||||
= other_roas_covering_this_prefix.iter().filter(|other| {
|
||||
other.asn == roa.origin()
|
||||
&& other.prefix.addr_len() <= roa.prefix.addr_len()
|
||||
&& other.effective_max_length() >= roa.effective_max_len()
|
||||
}).copied().collect::<Vec<_>>();
|
||||
|
||||
// Find all route origins that are made valid by this ROA.
|
||||
//
|
||||
// (Using filter and then map here makes the code quite a bit
|
||||
// easier ...)
|
||||
let authorizes = covered.iter().filter(|origin| {
|
||||
matches!(origin.validity, RouteOriginValidity::Valid(_))
|
||||
&& origin.route_origin.prefix.addr_len()
|
||||
<= roa.effective_max_len()
|
||||
&& origin.route_origin.origin == roa.origin()
|
||||
}).map(|origin| origin.announcement()).collect::<Vec<_>>();
|
||||
|
||||
// Find all route origins that are made invalid by this ROA.
|
||||
let disallows = covered.iter().filter(|origin| {
|
||||
matches!(
|
||||
origin.validity,
|
||||
RouteOriginValidity::InvalidLength
|
||||
| RouteOriginValidity::InvalidAsn
|
||||
)
|
||||
}).map(|origin| origin.announcement()).collect::<Vec<_>>();
|
||||
|
||||
// Is the ROA too permissive?
|
||||
//
|
||||
// XXX: I don’t understand why this does what it does.
|
||||
let authorizes_excess = {
|
||||
let max_len = roa.effective_max_len();
|
||||
let nr_of_specific_origins = u128::try_from(
|
||||
authorizes.iter().filter(|origin| {
|
||||
origin.prefix.addr_len() == max_len
|
||||
}).count()
|
||||
).unwrap_or(u128::MAX);
|
||||
|
||||
nr_of_specific_origins > 0
|
||||
&& nr_of_specific_origins
|
||||
< roa.payload().nr_of_specific_prefixes()
|
||||
};
|
||||
|
||||
// Now we have everything we need to categorize the ROA.
|
||||
if roa.origin() == AsNumber::AS0 {
|
||||
if other_roas_covering_this_prefix.is_empty() {
|
||||
// Disallows all covered route origins.
|
||||
BgpAnalysisEntry::roa_as0(
|
||||
roa.roa.clone(),
|
||||
covered.iter().map(|origin| {
|
||||
origin.announcement()
|
||||
}).collect()
|
||||
)
|
||||
}
|
||||
else {
|
||||
// This AS0 ROA is redundant.
|
||||
BgpAnalysisEntry::roa_as0_redundant(
|
||||
roa.roa.clone(),
|
||||
other_roas_covering_this_prefix,
|
||||
)
|
||||
}
|
||||
}
|
||||
else if !other_roas_including_this_definition.is_empty() {
|
||||
BgpAnalysisEntry::roa_redundant(
|
||||
roa.roa.clone(),
|
||||
authorizes,
|
||||
disallows,
|
||||
other_roas_including_this_definition
|
||||
)
|
||||
}
|
||||
else if authorizes.is_empty() && disallows.is_empty() {
|
||||
BgpAnalysisEntry::roa_unseen(roa.roa.clone())
|
||||
}
|
||||
else if authorizes_excess {
|
||||
BgpAnalysisEntry::roa_too_permissive(
|
||||
roa.roa.clone(), authorizes, disallows
|
||||
)
|
||||
}
|
||||
else if authorizes.is_empty() {
|
||||
BgpAnalysisEntry::roa_disallowing(roa.roa.clone(), disallows)
|
||||
}
|
||||
else {
|
||||
BgpAnalysisEntry::roa_seen(roa.roa.clone(), authorizes, disallows)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
//------------ Roa -----------------------------------------------------------
|
||||
|
||||
/// A configured ROA plus its address prefix.
|
||||
///
|
||||
/// This type only exists to be generic over the address family. Thus, `P`
|
||||
/// can either be [`Ipv4Prefix`] or [`Ipv6Prefix`].
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Roa<'a, P> {
|
||||
/// The address prefix of the ROA.
|
||||
prefix: P,
|
||||
|
||||
/// A reference to the actual ROA.
|
||||
roa: &'a ConfiguredRoa,
|
||||
}
|
||||
|
||||
impl<'a, P: RoutePrefix> Roa<'a, P> {
|
||||
/// Creates a new value from its parts.
|
||||
fn new(prefix: P, roa: &'a ConfiguredRoa) -> Self {
|
||||
Self { prefix, roa }
|
||||
}
|
||||
|
||||
/// Returns the ROA payload definition of the ROA.
|
||||
fn payload(self) -> RoaPayload {
|
||||
self.roa.roa_configuration.payload
|
||||
}
|
||||
|
||||
/// Returns the origin AS number of the ROA definition.
|
||||
fn origin(self) -> AsNumber {
|
||||
self.roa.roa_configuration.payload.asn
|
||||
}
|
||||
|
||||
/// Retuns the maximum prefix length of the ROA definition.
|
||||
fn max_len(self) -> Option<u8> {
|
||||
self.roa.roa_configuration.payload.max_length
|
||||
}
|
||||
|
||||
/// Returns the effective maximum prefix length of the ROA definition.
|
||||
///
|
||||
/// This is the maximum prefix length if provided or the address prefix
|
||||
/// length otherwise.
|
||||
fn effective_max_len(self) -> u8 {
|
||||
self.max_len().unwrap_or(self.prefix.addr_len())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ ValidatedRouteOrigin ------------------------------------------
|
||||
|
||||
/// A route origin with route origin validation applied to it.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ValidatedRouteOrigin<P> {
|
||||
/// The route origin, i.e., address prefix and origin AS number.
|
||||
route_origin: RouteOrigin<P>,
|
||||
|
||||
/// The validation status of the route origin.
|
||||
validity: RouteOriginValidity,
|
||||
|
||||
/// The ROAs that contributed to invalidating the route origin.
|
||||
disallowing: Vec<RoaPayload>,
|
||||
}
|
||||
|
||||
impl<P: RoutePrefix> ValidatedRouteOrigin<P> {
|
||||
/// Validates a set of route origin against a set of ROAs.
|
||||
///
|
||||
/// Appends the validation verdict for each route origin to the end of
|
||||
/// `target`.
|
||||
fn validate_set(
|
||||
route_origins: RouteOriginSet<P>,
|
||||
roas: &[Roa<P>],
|
||||
target: &mut Vec<Self>,
|
||||
) {
|
||||
// Find all ROAs that cover the route origin’s prefix.
|
||||
let covering = roas.iter().copied().filter(|roa| {
|
||||
roa.prefix.covers(route_origins.prefix())
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
// If there aren’t any, all route origins in the set are unknown.
|
||||
if covering.is_empty() {
|
||||
target.extend(route_origins.iter().map(|origin| {
|
||||
Self {
|
||||
route_origin: origin,
|
||||
validity: RouteOriginValidity::NotFound,
|
||||
disallowing: Vec::new(),
|
||||
}
|
||||
}));
|
||||
return
|
||||
}
|
||||
|
||||
for origin in route_origins.iter() {
|
||||
target.push(Self::validate(origin, &covering));
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates a single route origin against a set of ROAs.
|
||||
///
|
||||
/// Returns the verdict.
|
||||
fn validate(
|
||||
origin: RouteOrigin<P>,
|
||||
covering: &[Roa<P>]
|
||||
) -> Self {
|
||||
let mut invalidating = Vec::new();
|
||||
let mut same_asn_found = false;
|
||||
let mut none_as0_found = false;
|
||||
for roa in covering.iter().copied() {
|
||||
if roa.origin() == origin.origin {
|
||||
if roa.prefix.covers(origin.prefix)
|
||||
&& roa.effective_max_len() >= origin.prefix.addr_len()
|
||||
{
|
||||
return Self {
|
||||
route_origin: origin,
|
||||
validity: RouteOriginValidity::Valid(roa.payload()),
|
||||
disallowing: Vec::new(),
|
||||
}
|
||||
}
|
||||
else {
|
||||
same_asn_found = true;
|
||||
}
|
||||
}
|
||||
if roa.origin() != AsNumber::AS0 {
|
||||
none_as0_found = true;
|
||||
}
|
||||
invalidating.push(roa.payload());
|
||||
}
|
||||
|
||||
Self {
|
||||
route_origin: origin,
|
||||
validity: if same_asn_found {
|
||||
RouteOriginValidity::InvalidLength
|
||||
}
|
||||
else if none_as0_found {
|
||||
RouteOriginValidity::InvalidAsn
|
||||
}
|
||||
else {
|
||||
RouteOriginValidity::Disallowed
|
||||
},
|
||||
disallowing: invalidating,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the announcement correlating with the route origin.
|
||||
///
|
||||
/// “Announcement” is the term used in the API for a route origin.
|
||||
fn announcement(&self) -> Announcement {
|
||||
self.route_origin.into()
|
||||
}
|
||||
|
||||
/// Converts the value into a BGP analysis entry.
|
||||
fn into_analysis_entry(self) -> BgpAnalysisEntry {
|
||||
match self.validity {
|
||||
RouteOriginValidity::Valid(roa) => {
|
||||
BgpAnalysisEntry::announcement_valid(
|
||||
self.route_origin.into(), roa
|
||||
)
|
||||
}
|
||||
RouteOriginValidity::Disallowed => {
|
||||
BgpAnalysisEntry::announcement_disallowed(
|
||||
self.route_origin.into(),
|
||||
self.disallowing,
|
||||
)
|
||||
}
|
||||
RouteOriginValidity::InvalidLength => {
|
||||
BgpAnalysisEntry::announcement_invalid_length(
|
||||
self.route_origin.into(),
|
||||
self.disallowing,
|
||||
)
|
||||
}
|
||||
RouteOriginValidity::InvalidAsn => {
|
||||
BgpAnalysisEntry::announcement_invalid_asn(
|
||||
self.route_origin.into(),
|
||||
self.disallowing,
|
||||
)
|
||||
}
|
||||
RouteOriginValidity::NotFound => {
|
||||
BgpAnalysisEntry::announcement_not_found(
|
||||
self.route_origin.into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ RouteOriginValidity -------------------------------------------
|
||||
|
||||
/// The status of a route origin after validation.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum RouteOriginValidity {
|
||||
/// The route origin is valid.
|
||||
///
|
||||
/// The included ROA is the one that made the origin valid.
|
||||
Valid(RoaPayload),
|
||||
|
||||
/// The route origin is invalid due to having an invalid prefix length.
|
||||
InvalidLength,
|
||||
|
||||
/// The route origin is invalid due to having an invalid origin AS number.
|
||||
InvalidAsn,
|
||||
|
||||
/// The route origin is invalid due to an AS0 ROA.
|
||||
Disallowed,
|
||||
|
||||
/// No covering ROA exists and the route origin is “not found.”
|
||||
NotFound,
|
||||
}
|
||||
|
||||
|
||||
//------------ Tests --------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use crate::api::roa::RoaConfigurationUpdates;
|
||||
use crate::commons::test::{configured_roa};
|
||||
use super::super::riswhois::RouteOriginCollection;
|
||||
use super::*;
|
||||
|
||||
fn ann(s: &str) -> Announcement {
|
||||
Announcement::from_str(s).unwrap()
|
||||
}
|
||||
|
||||
fn test_analyser() -> BgpAnalyser {
|
||||
fn origin<P>(prefix: &str, origin: u32) -> RouteOrigin<P>
|
||||
where
|
||||
P: FromStr,
|
||||
<P as FromStr>::Err: fmt::Debug
|
||||
{
|
||||
RouteOrigin {
|
||||
prefix: P::from_str(prefix).unwrap(),
|
||||
origin: AsNumber::from_u32(origin)
|
||||
}
|
||||
}
|
||||
|
||||
BgpAnalyser {
|
||||
loader: None,
|
||||
refresh_interval: Duration::seconds(12),
|
||||
last_checked: i64::MIN.into(),
|
||||
riswhois: ArcSwapOption::new(Some(Arc::new(RisWhois::new(
|
||||
RouteOriginCollection::new(
|
||||
vec![
|
||||
origin("10.0.0.0/22", 64496),
|
||||
origin("10.0.2.0/23", 64496),
|
||||
origin("10.0.0.0/24", 64496),
|
||||
origin("10.0.0.0/22", 64497),
|
||||
origin("10.0.0.0/21", 64497),
|
||||
origin("192.168.0.0/24", 64497),
|
||||
origin("192.168.0.0/24", 64496),
|
||||
origin("192.168.1.0/24", 64497),
|
||||
]
|
||||
).unwrap(),
|
||||
RouteOriginCollection::new(
|
||||
vec![
|
||||
origin("2001:DB8::/32", 64498),
|
||||
]
|
||||
).unwrap(),
|
||||
))))
|
||||
}
|
||||
}
|
||||
|
||||
fn test_analyser_full() -> BgpAnalyser {
|
||||
let v4 = RisWhoisLoader::parse_data(include_bytes!(
|
||||
"../../../test-resources/bgp/riswhoisdump.IPv4"
|
||||
).as_ref()).unwrap();
|
||||
let v6 = RisWhoisLoader::parse_data(include_bytes!(
|
||||
"../../../test-resources/bgp/riswhoisdump.IPv6"
|
||||
).as_ref()).unwrap();
|
||||
let ris = RisWhois::new(v4, v6);
|
||||
|
||||
BgpAnalyser {
|
||||
loader: None,
|
||||
refresh_interval: Duration::seconds(12),
|
||||
last_checked: i64::MIN.into(),
|
||||
riswhois: ArcSwapOption::new(Some(Arc::new(ris))),
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_analyser() -> BgpAnalyser {
|
||||
BgpAnalyser {
|
||||
loader: None,
|
||||
refresh_interval: Duration::seconds(12),
|
||||
last_checked: i64::MIN.into(),
|
||||
riswhois: ArcSwapOption::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyse_bgp() {
|
||||
let roa_too_permissive = configured_roa("10.0.0.0/22-23 => 64496");
|
||||
let roa_as0 = configured_roa("10.0.4.0/24 => 0");
|
||||
let roa_unseen_completely = configured_roa("10.0.3.0/24 => 64497");
|
||||
|
||||
let roa_not_held = configured_roa("10.1.0.0/24 => 64497");
|
||||
|
||||
let roa_authorizing_single =
|
||||
configured_roa("192.168.1.0/24 => 64497");
|
||||
let roa_unseen_redundant = configured_roa("192.168.1.0/24 => 64498");
|
||||
let roa_as0_redundant = configured_roa("192.168.1.0/24 => 0");
|
||||
|
||||
let resources_held =
|
||||
ResourceSet::from_strs("", "10.0.0.0/16, 192.168.0.0/16", "")
|
||||
.unwrap();
|
||||
let limit = None;
|
||||
|
||||
let analyser = test_analyser();
|
||||
|
||||
let report = analyser.analyse(
|
||||
&[
|
||||
roa_too_permissive,
|
||||
roa_as0,
|
||||
roa_unseen_completely,
|
||||
roa_not_held,
|
||||
roa_authorizing_single,
|
||||
roa_unseen_redundant,
|
||||
roa_as0_redundant,
|
||||
],
|
||||
&resources_held,
|
||||
limit,
|
||||
);
|
||||
|
||||
let expected: BgpAnalysisReport = serde_json::from_str(include_str!(
|
||||
"../../../test-resources/bgp/expected_full_report.json"
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(report, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyse_bgp_disallowed_announcements() {
|
||||
let roa = configured_roa("10.0.0.0/22 => 0");
|
||||
|
||||
let roas = &[roa];
|
||||
let analyser = test_analyser();
|
||||
|
||||
let resources_held =
|
||||
ResourceSet::from_strs("", "10.0.0.0/8, 192.168.0.0/16", "")
|
||||
.unwrap();
|
||||
let report = analyser.analyse(roas, &resources_held, None);
|
||||
|
||||
assert!(!report.contains_invalids());
|
||||
|
||||
let mut disallowed = report
|
||||
.matching_announcements(BgpAnalysisState::AnnouncementDisallowed);
|
||||
disallowed.sort();
|
||||
|
||||
let disallowed_1 = ann("10.0.0.0/22 => 64496");
|
||||
let disallowed_2 = ann("10.0.0.0/22 => 64497");
|
||||
let disallowed_3 = ann("10.0.0.0/24 => 64496");
|
||||
let disallowed_4 = ann("10.0.2.0/23 => 64496");
|
||||
let mut expected =
|
||||
vec![disallowed_1, disallowed_2, disallowed_3, disallowed_4];
|
||||
expected.sort();
|
||||
|
||||
assert_eq!(disallowed, expected);
|
||||
|
||||
// The suggestion should not try to add the disallowed announcements
|
||||
// because they were disallowed by an AS0 roa.
|
||||
let suggestion = analyser.suggest(roas, &resources_held, None);
|
||||
let updates = RoaConfigurationUpdates::from(suggestion);
|
||||
|
||||
let added = &updates.added;
|
||||
for announcement in disallowed {
|
||||
assert!(!added.iter().any(|added_roa| {
|
||||
let added_payload = added_roa.payload;
|
||||
let announcement_payload = RoaPayload::from(announcement);
|
||||
added_payload.includes(announcement_payload)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyse_bgp_no_announcements() {
|
||||
let roa1 = configured_roa("10.0.0.0/23-24 => 64496");
|
||||
let roa2 = configured_roa("10.0.3.0/24 => 64497");
|
||||
let roa3 = configured_roa("10.0.4.0/24 => 0");
|
||||
|
||||
let roas = vec![roa1, roa2, roa3];
|
||||
|
||||
let resources_held =
|
||||
ResourceSet::from_strs("", "10.0.0.0/16", "").unwrap();
|
||||
|
||||
let analyser = empty_analyser();
|
||||
let table = analyser.analyse(&roas, &resources_held, None);
|
||||
let table_entries = table.entries();
|
||||
assert_eq!(3, table_entries.len());
|
||||
|
||||
let roas_no_info: Vec<ConfiguredRoa> = table_entries
|
||||
.iter()
|
||||
.filter(|e| e.state() == BgpAnalysisState::RoaNoAnnouncementInfo)
|
||||
.map(|e| e.configured_roa().clone())
|
||||
.collect();
|
||||
|
||||
assert_eq!(roas_no_info, roas);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_bgp_analysis_suggestion() {
|
||||
let roa_too_permissive = configured_roa("10.0.0.0/22-23 => 64496");
|
||||
let roa_redundant = configured_roa("10.0.0.0/23 => 64496");
|
||||
let roa_as0 = configured_roa("10.0.4.0/24 => 0");
|
||||
let roa_unseen_completely = configured_roa("10.0.3.0/24 => 64497");
|
||||
let roa_authorizing_single =
|
||||
configured_roa("192.168.1.0/24 => 64497");
|
||||
let roa_unseen_redundant = configured_roa("192.168.1.0/24 => 64498");
|
||||
let roa_as0_redundant = configured_roa("192.168.1.0/24 => 0");
|
||||
|
||||
let roas = &[
|
||||
roa_too_permissive,
|
||||
roa_redundant,
|
||||
roa_as0,
|
||||
roa_unseen_completely,
|
||||
roa_authorizing_single,
|
||||
roa_unseen_redundant,
|
||||
roa_as0_redundant,
|
||||
];
|
||||
|
||||
let analyser = test_analyser();
|
||||
|
||||
let resources_held =
|
||||
ResourceSet::from_strs("", "10.0.0.0/8, 192.168.0.0/16", "")
|
||||
.unwrap();
|
||||
let limit =
|
||||
Some(ResourceSet::from_strs("", "10.0.0.0/22", "").unwrap());
|
||||
let suggestion_resource_subset =
|
||||
analyser.suggest(roas, &resources_held, limit);
|
||||
|
||||
let expected: BgpAnalysisSuggestion =
|
||||
serde_json::from_str(include_str!(
|
||||
"../../../test-resources/bgp/expected_suggestion_some_roas.json"
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(suggestion_resource_subset, expected);
|
||||
|
||||
let suggestion_all_roas_in_scope =
|
||||
analyser.suggest(roas, &resources_held, None);
|
||||
|
||||
let expected: BgpAnalysisSuggestion =
|
||||
serde_json::from_str(include_str!(
|
||||
"../../../test-resources/bgp/expected_suggestion_all_roas.json"
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(suggestion_all_roas_in_scope, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyse_nlnet_labs_snapshot() {
|
||||
let analyser = test_analyser_full();
|
||||
|
||||
let asns = "AS204325, AS211321";
|
||||
let ipv4s = "185.49.140.0/22";
|
||||
let ipv6s = "2a04:b900::/29";
|
||||
let set = ResourceSet::from_strs(asns, ipv4s, ipv6s).unwrap();
|
||||
|
||||
let roas = &[
|
||||
configured_roa("2a04:b906::/48-48 => 0"),
|
||||
configured_roa("2a04:b907::/48-48 => 0"),
|
||||
configured_roa("185.49.142.0/24-24 => 0"),
|
||||
configured_roa("2a04:b900::/30-32 => 8587"),
|
||||
configured_roa("185.49.140.0/23-23 => 8587"),
|
||||
configured_roa("2a04:b900::/30-30 => 8587"),
|
||||
configured_roa("2a04:b905::/48-48 => 16509"),
|
||||
configured_roa("2a04:b904::/48-48 => 211321"),
|
||||
configured_roa("2a04:b907::/47-47 => 211321"),
|
||||
configured_roa("185.49.142.0/23-23 => 211321"),
|
||||
configured_roa("2a04:b902::/48-48 => 211321"),
|
||||
configured_roa("185.49.143.0/24-24 => 211321"),
|
||||
];
|
||||
|
||||
let report = analyser.analyse(roas, &set, None);
|
||||
|
||||
let entry_expect_roa = |x: &str, y| {
|
||||
let x = x.to_string();
|
||||
dbg!(&x, &y);
|
||||
assert!(report.entries().iter().any(|s|
|
||||
s.state() == y &&
|
||||
s.configured_roa().to_string() == x
|
||||
));
|
||||
};
|
||||
|
||||
let entry_expect_ann = |x: &str, y: u32, z: BgpAnalysisState| {
|
||||
let x = x.to_string();
|
||||
dbg!(&x, &y, &z);
|
||||
assert!(report.entries().iter().any(|s|
|
||||
s.state() == z &&
|
||||
s.announcement().asn == AsNumber::from_u32(y) &&
|
||||
s.announcement().prefix.to_string() == x
|
||||
));
|
||||
};
|
||||
|
||||
entry_expect_roa(
|
||||
"2a04:b906::/48-48 => 0", BgpAnalysisState::RoaAs0
|
||||
);
|
||||
entry_expect_roa(
|
||||
"2a04:b907::/48-48 => 0", BgpAnalysisState::RoaAs0Redundant
|
||||
);
|
||||
entry_expect_roa(
|
||||
"185.49.142.0/24-24 => 0", BgpAnalysisState::RoaAs0Redundant
|
||||
);
|
||||
entry_expect_roa(
|
||||
"2a04:b900::/30-32 => 8587", BgpAnalysisState::RoaSeen
|
||||
);
|
||||
entry_expect_roa(
|
||||
"185.49.140.0/23-23 => 8587", BgpAnalysisState::RoaSeen
|
||||
);
|
||||
entry_expect_roa(
|
||||
"2a04:b900::/30-30 => 8587", BgpAnalysisState::RoaRedundant
|
||||
);
|
||||
entry_expect_roa(
|
||||
"2a04:b905::/48-48 => 16509", BgpAnalysisState::RoaSeen
|
||||
);
|
||||
entry_expect_roa(
|
||||
"2a04:b904::/48-48 => 211321", BgpAnalysisState::RoaSeen
|
||||
);
|
||||
entry_expect_roa(
|
||||
"2a04:b907::/47-47 => 211321", BgpAnalysisState::RoaSeen
|
||||
);
|
||||
entry_expect_roa(
|
||||
"185.49.142.0/23-23 => 211321", BgpAnalysisState::RoaSeen
|
||||
);
|
||||
entry_expect_ann(
|
||||
"2a04:b907::/48", 211321,
|
||||
BgpAnalysisState::AnnouncementInvalidLength
|
||||
);
|
||||
entry_expect_ann(
|
||||
"185.49.142.0/24", 211321,
|
||||
BgpAnalysisState::AnnouncementInvalidLength
|
||||
);
|
||||
entry_expect_roa(
|
||||
"2a04:b902::/48-48 => 211321", BgpAnalysisState::RoaUnseen
|
||||
);
|
||||
entry_expect_roa(
|
||||
"185.49.143.0/24-24 => 211321", BgpAnalysisState::RoaUnseen
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
//! The analyser for checking and suggesting ROAs.
|
||||
//!
|
||||
//! The analyser, [`BgpAnalyser`], downloads RISwhois dumps which contain
|
||||
//! prefixes and origins seen in real BGP data by RIS and stores them in
|
||||
//! memory. Based on this data, it checks whether the ROAs for a given CA
|
||||
//! reflect what is seen by RIS and can make suggestions which ROAs should
|
||||
//! be created.
|
||||
|
||||
pub use self::analyser::BgpAnalyser;
|
||||
pub use self::riswhois::RisWhoisError;
|
||||
|
||||
mod analyser;
|
||||
mod riswhois;
|
||||
|
||||
@@ -0,0 +1,986 @@
|
||||
//! The data from a RISwhois data set.
|
||||
//!
|
||||
//! These datasets provide the originating AS numbers for address prefixes
|
||||
//! as encountered in BGP data collected by RIS. The [`RisWhois`] type in
|
||||
//! this module collects all this data and makes it available for querying.
|
||||
|
||||
// This code only works with `usize` of at least 32 bits.
|
||||
#[cfg(target_pointer_width = "16")]
|
||||
compile_error!("cannot build on 16 bit systems");
|
||||
|
||||
use std::{cmp, error, fmt, io};
|
||||
use std::io::BufReader;
|
||||
use std::str::FromStr;
|
||||
use libflate::gzip;
|
||||
use crate::api::roa::{AsNumber, Ipv4Prefix, Ipv6Prefix, TypedPrefix};
|
||||
use crate::api::bgp::Announcement;
|
||||
|
||||
|
||||
//------------ Configuration -------------------------------------------------
|
||||
|
||||
/// How often to we need to see a route origin before accepting it.
|
||||
///
|
||||
/// For each pair of address prefix and origin AS number, RISwhois also lists
|
||||
/// how many of the peers of RIS have seen this pair in their BGP streams.
|
||||
/// Pairs that are only seen by very few peers are likely there by mistake
|
||||
/// and should be filtered out. This constant sets the minimum number of
|
||||
/// stream that have to have seen a pair for us to include it in our data.
|
||||
///
|
||||
/// This number was at some point recommended by RIS.
|
||||
const MINIMUM_SEEN_BY: u32 = 13;
|
||||
|
||||
|
||||
//------------ RisWhoisLoader ------------------------------------------------
|
||||
|
||||
/// A type that knows where RISwhois data lives and download it.
|
||||
pub struct RisWhoisLoader {
|
||||
/// The HTTP(S) URL of the location of IPv4 data set.
|
||||
v4_url: String,
|
||||
|
||||
/// The HTTP(S) URL of the location of IPv6 data set.
|
||||
v6_url: String,
|
||||
}
|
||||
|
||||
impl RisWhoisLoader {
|
||||
/// Creates a new loader from the URLS of the IPv4 and IPv6 data sets.
|
||||
pub fn new(v4_url: String, v6_url: String) -> Self {
|
||||
Self { v4_url, v6_url }
|
||||
}
|
||||
|
||||
/// Downloads and processes a new data set.
|
||||
pub async fn load(&self) -> Result<RisWhois, RisWhoisError> {
|
||||
Ok(RisWhois::new(
|
||||
Self::load_tree(&self.v4_url).await?,
|
||||
Self::load_tree(&self.v6_url).await?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Downloads and process the tree for one address family.
|
||||
async fn load_tree<P: FromStr + RoutePrefix>(
|
||||
uri: &str
|
||||
) -> Result<RouteOriginCollection<P>, RisWhoisError>
|
||||
where <P as FromStr>::Err: error::Error + Send + Sync + 'static {
|
||||
Self::parse_gz_data(
|
||||
&reqwest::get(uri).await.map_err(|err| {
|
||||
RisWhoisError::new(uri, io::Error::other(err))
|
||||
})?.bytes().await.map_err(|err| {
|
||||
RisWhoisError::new(uri, io::Error::other(err))
|
||||
})?
|
||||
).map_err(|err| RisWhoisError::new(uri, err))
|
||||
}
|
||||
|
||||
/// Parses the gzipped data.
|
||||
fn parse_gz_data<P: FromStr + RoutePrefix>(
|
||||
data: &[u8]
|
||||
) -> Result<RouteOriginCollection<P>, io::Error>
|
||||
where <P as FromStr>::Err: error::Error + Send + Sync + 'static {
|
||||
let data = BufReader::new(
|
||||
gzip::Decoder::new(data)?
|
||||
);
|
||||
Self::parse_data(data)
|
||||
}
|
||||
|
||||
/// Parses the raw data.
|
||||
pub(super) fn parse_data<P: FromStr + RoutePrefix>(
|
||||
data: impl io::BufRead,
|
||||
) -> Result<RouteOriginCollection<P>, io::Error>
|
||||
where <P as FromStr>::Err: error::Error + Send + Sync + 'static {
|
||||
let mut res = Vec::new();
|
||||
for line in data.lines() {
|
||||
// Each line is as follows:
|
||||
//
|
||||
// o empty lines and lines starting with % are ignored.
|
||||
// o all other lines consist of three string separated by
|
||||
// white space (technically: a single HTAB):
|
||||
//
|
||||
// o origin AS number as an integer,
|
||||
// o prefix as IP address slash prefix length,
|
||||
// o number of peers that have seen this pair.
|
||||
//
|
||||
// Instead of the origin AS number, there may be an AS set
|
||||
// as a sequence of comma separated AS numbers surrounded by
|
||||
// curly braces. We ignore those.
|
||||
//
|
||||
// If the number of peers that have seen a pair is smaller
|
||||
// than `MINIMUM_SEEN_BY`, the line is also ignored.
|
||||
|
||||
let line = line?;
|
||||
if line.is_empty() || line.starts_with('%') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut values = line.split_whitespace();
|
||||
|
||||
let asn_str = values.next().ok_or(
|
||||
io::Error::other("missing column")
|
||||
)?;
|
||||
let prefix_str = values.next().ok_or(
|
||||
io::Error::other("missing column")
|
||||
)?;
|
||||
let peers = values.next().ok_or(
|
||||
io::Error::other("missing column")
|
||||
)?;
|
||||
|
||||
if u32::from_str(peers).map_err(io::Error::other)?
|
||||
< MINIMUM_SEEN_BY
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if asn_str.contains('{') {
|
||||
continue; // assets not supported (not important here either)
|
||||
}
|
||||
|
||||
let origin = AsNumber::from_str(asn_str).map_err(io::Error::other)?;
|
||||
let prefix = P::from_str(prefix_str).map_err(|err| {
|
||||
io::Error::other(err)
|
||||
})?;
|
||||
|
||||
res.push(RouteOrigin { prefix, origin });
|
||||
}
|
||||
|
||||
Ok(RouteOriginCollection::new(res).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ RisWhois ------------------------------------------------------
|
||||
|
||||
/// A set of RISwhois data.
|
||||
///
|
||||
/// This data consists of two route origin collections, one for IPv4 and one
|
||||
/// for IPv6.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct RisWhois {
|
||||
/// The IPv4 route origin collection.
|
||||
v4: RouteOriginCollection<Ipv4Prefix>,
|
||||
|
||||
/// The IPv6 route origin collection.
|
||||
v6: RouteOriginCollection<Ipv6Prefix>,
|
||||
}
|
||||
|
||||
impl RisWhois {
|
||||
/// Creates a new data set from the IPv4 and IPv6 data.
|
||||
pub fn new(
|
||||
v4: RouteOriginCollection<Ipv4Prefix>,
|
||||
v6: RouteOriginCollection<Ipv6Prefix>,
|
||||
) -> Self {
|
||||
Self { v4, v6 }
|
||||
}
|
||||
|
||||
/// Returns the IPv4 route origin collection.
|
||||
pub fn v4(&self) -> &RouteOriginCollection<Ipv4Prefix> {
|
||||
&self.v4
|
||||
}
|
||||
|
||||
/// Returns the IPv6 route origin collection.
|
||||
pub fn v6(&self) -> &RouteOriginCollection<Ipv6Prefix> {
|
||||
&self.v6
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ RouteOriginCollection -----------------------------------------
|
||||
|
||||
/// A collection of RISwhois route origins.
|
||||
///
|
||||
/// This type keeps the route origins for the prefix type `P`, which can be
|
||||
/// [`Ipv4Prefix`] or [`Ipv6Prefix`]. It is read-only, allowing to iterate
|
||||
/// over part of the data.
|
||||
///
|
||||
/// It currently only supports iterating over the more specifics of a given
|
||||
/// prefix since that is all we need for the BGP analyser.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RouteOriginCollection<P> {
|
||||
/// The tree part of the collection.
|
||||
///
|
||||
/// The tree nodes are essentially three pointers (but we are using
|
||||
/// 32 bits to save space): a pointer into data or no-data containing the
|
||||
/// prefix for the node plus the origin if it points into actual data,
|
||||
/// a pointer to the left child, and a pointer to the right child.
|
||||
///
|
||||
/// The left child is the the nearest longer prefix where the next bit
|
||||
/// (i.e., the bit at the bit position equal to this prefix’ length) is
|
||||
/// zero. The right child has that bit at one. They can, of course, be
|
||||
/// “none.”
|
||||
///
|
||||
/// “No-data” nodes are added for nodes in the tree whose prefix doesn’t
|
||||
/// appear in the data but which are necessary to make the prefix tree
|
||||
/// work. Unnecessary no-data node -- those that have only a left child
|
||||
/// or only a right child -- are skipped. The result is that a child
|
||||
/// node isn’t necessarily for the prefix with an address length plus
|
||||
/// one. This is why we need to keep the prefixes.
|
||||
///
|
||||
/// A possible optimization (for later) would be to not keep the prefixes
|
||||
/// for direct children of a node since we can determine the prefix from
|
||||
/// the parent plus whether it is a left or right child. However, that
|
||||
/// makes the tree creation algorithm much more complicated, so this has
|
||||
/// not yet been done.
|
||||
tree: Box<[TreeNode]>,
|
||||
|
||||
/// The index in `tree` of the root node.
|
||||
///
|
||||
/// The root node will always be `P::default()`, i.e., the “0/0” prefix.
|
||||
/// If it isn’t part of the data, it will be an artifical no-data node.
|
||||
tree_root_idx: TreeIndex,
|
||||
|
||||
/// The boxed slice of the data.
|
||||
data: RouteOriginBox<P>,
|
||||
|
||||
/// The boxed slice of the “no-data.”
|
||||
///
|
||||
/// This contains prefixes of the empty nodes that we had to add to make
|
||||
/// the tree work.
|
||||
no_data: Box<[P]>,
|
||||
}
|
||||
|
||||
impl<P: RoutePrefix> RouteOriginCollection<P> {
|
||||
/// Creates a new collection from the given list of route origins.
|
||||
pub fn new(data: Vec<RouteOrigin<P>>) -> Result<Self, LargeDataset> {
|
||||
CollectionBuilder::new(data).process()
|
||||
}
|
||||
|
||||
/// Returns an iterator over all equal or more sepcific route origins.
|
||||
///
|
||||
/// The iterator will start at the origin for the prefix itself, if
|
||||
/// present, and walk the more specific in prefix order.
|
||||
pub fn eq_or_more_specific(&self, prefix: P) -> TreeIter<'_, P> {
|
||||
TreeIter::more_specific(self, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: RoutePrefix> RouteOriginCollection<P> {
|
||||
/// Returns the node for the given tree index of available.
|
||||
fn get_tree_node(&self, tree_idx: TreeIndex) -> Option<TreeNode> {
|
||||
self.tree.get(tree_idx.into_usize()?).copied()
|
||||
}
|
||||
|
||||
/// Returns the prefix for the given data index of available.
|
||||
fn get_data_prefix(&self, data_idx: DataIndex) -> Option<P> {
|
||||
match data_idx.into_data() {
|
||||
Ok(data_idx) => Some(self.data.0.get(data_idx)?.prefix),
|
||||
Err(no_data_idx) => self.no_data.get(no_data_idx).copied(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> Default for RouteOriginCollection<P> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tree: Box::new([]),
|
||||
tree_root_idx: TreeIndex::none(),
|
||||
data: RouteOriginBox::default(),
|
||||
no_data: Box::new([]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ CollectionBuilder ---------------------------------------------
|
||||
|
||||
/// A builder for a route origin collection.
|
||||
struct CollectionBuilder<P> {
|
||||
/// The tree part of the collection.
|
||||
///
|
||||
/// See [`RouteOriginCollection`] for details.
|
||||
tree: Vec<TreeNode>,
|
||||
|
||||
/// The no-data prefixes.
|
||||
///
|
||||
/// See [`RouteOriginCollection`] for details.
|
||||
no_data: Vec<P>,
|
||||
|
||||
/// The data route origins.
|
||||
///
|
||||
/// See [`RouteOriginCollection`] for details.
|
||||
data: RouteOriginBox<P>,
|
||||
|
||||
/// The index in `data` with the item next to process.
|
||||
next_data_idx: usize,
|
||||
}
|
||||
|
||||
impl<P: RoutePrefix> CollectionBuilder<P> {
|
||||
/// Creates a new collection builder from the route origins.
|
||||
fn new(data: Vec<RouteOrigin<P>>) -> Self {
|
||||
let mut data = data.into_boxed_slice();
|
||||
data.sort();
|
||||
let data = RouteOriginBox(data);
|
||||
|
||||
Self {
|
||||
tree: Vec::new(),
|
||||
no_data: Vec::new(),
|
||||
data,
|
||||
next_data_idx: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates and returns a collection from the builder.
|
||||
fn process(mut self) -> Result<RouteOriginCollection<P>, LargeDataset> {
|
||||
let Some(prefix) = self.next_prefix() else {
|
||||
return Ok(RouteOriginCollection {
|
||||
tree: Box::new([]),
|
||||
tree_root_idx: TreeIndex::none(),
|
||||
data: RouteOriginBox(Box::new([])),
|
||||
no_data: Box::new([]),
|
||||
})
|
||||
};
|
||||
|
||||
let node = if prefix == P::default() {
|
||||
self.advance_data();
|
||||
self.process_node(
|
||||
prefix, TreeNode::new(DataIndex::data(0)?)
|
||||
)?
|
||||
}
|
||||
else {
|
||||
let data = self.push_no_data(P::default())?;
|
||||
self.process_node(
|
||||
P::default(), TreeNode::new(data)
|
||||
)?
|
||||
};
|
||||
let tree_root_idx = self.push_node(node)?;
|
||||
Ok(RouteOriginCollection {
|
||||
tree: self.tree.into_boxed_slice(),
|
||||
tree_root_idx,
|
||||
data: self.data,
|
||||
no_data: self.no_data.into_boxed_slice()
|
||||
})
|
||||
}
|
||||
|
||||
/// Processes the given node.
|
||||
///
|
||||
/// Adds the necessary nodes and possibly creates no-data intermediary
|
||||
/// nodes. Returns the node that the caller needs to add to the tree.
|
||||
fn process_node(
|
||||
&mut self,
|
||||
prefix: P,
|
||||
mut node: TreeNode
|
||||
) -> Result<TreeNode, LargeDataset> {
|
||||
loop {
|
||||
// Get the next prefix or return the node as it is.
|
||||
let Some(next_prefix) = self.next_prefix() else {
|
||||
return Ok(node)
|
||||
};
|
||||
|
||||
// If we don’t cover the next prefix, return the node as it is.
|
||||
if !prefix.covers(next_prefix) {
|
||||
return Ok(node)
|
||||
}
|
||||
|
||||
if !next_prefix.bit(prefix.addr_len()) {
|
||||
// Next prefix doesn’t have the next bit set, so it is a left
|
||||
// child.
|
||||
if let Some(left_idx) = node.left.into_usize() {
|
||||
// If there already is a left child, we need to insert
|
||||
// an empty node at the closest ancestor of the left
|
||||
// child’s prefix and whatever the next prefix turns into.
|
||||
let ancestor_prefix = self.node_prefix(
|
||||
left_idx
|
||||
).closest_ancestor(next_prefix);
|
||||
let data = self.push_no_data(ancestor_prefix)?;
|
||||
let inter_node = self.process_node(
|
||||
ancestor_prefix,
|
||||
TreeNode::with_children(
|
||||
data, node.left, TreeIndex::none()
|
||||
)
|
||||
)?;
|
||||
node.left = self.push_node(inter_node)?;
|
||||
}
|
||||
else {
|
||||
// If there isn’t currently a left child, the next item
|
||||
// will become the left child.
|
||||
let left_node = TreeNode::new(
|
||||
DataIndex::data(self.next_data_idx)?
|
||||
);
|
||||
self.advance_data();
|
||||
let left_node = self.process_node(
|
||||
next_prefix, left_node,
|
||||
)?;
|
||||
node.left = self.push_node(left_node)?;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Next prefix doesn’t have the next bit set, so it is a right
|
||||
// child.
|
||||
if let Some(right_idx) = node.right.into_usize() {
|
||||
// If there already is a right child, we need an empty
|
||||
// node. The current right child will become the left
|
||||
// child of that node.
|
||||
let ancestor_prefix = self.node_prefix(
|
||||
right_idx
|
||||
).closest_ancestor(next_prefix);
|
||||
let data = self.push_no_data(ancestor_prefix)?;
|
||||
let inter_node = self.process_node(
|
||||
ancestor_prefix,
|
||||
TreeNode::with_children(
|
||||
data, node.right, TreeIndex::none()
|
||||
)
|
||||
)?;
|
||||
node.right = self.push_node(inter_node)?;
|
||||
}
|
||||
else {
|
||||
// No current right child. Add it.
|
||||
let right_node = TreeNode::new(
|
||||
DataIndex::data(self.next_data_idx)?
|
||||
);
|
||||
self.advance_data();
|
||||
let right_node = self.process_node(
|
||||
next_prefix, right_node
|
||||
)?;
|
||||
node.right = self.push_node(right_node)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the prefix of the next data item or `None` if we are done.
|
||||
fn next_prefix(&self) -> Option<P> {
|
||||
self.data.0.get(self.next_data_idx).map(|item| item.prefix)
|
||||
}
|
||||
|
||||
/// Advances the next data item.
|
||||
fn advance_data(&mut self) {
|
||||
self.next_data_idx = self.data.next_prefix(self.next_data_idx);
|
||||
}
|
||||
|
||||
/// Returns the node for the given tree node.
|
||||
fn node_prefix(&self, node_idx: usize) -> P {
|
||||
match self.tree[node_idx].data.into_data() {
|
||||
Ok(idx) => self.data.0[idx].prefix,
|
||||
Err(idx) => self.no_data[idx]
|
||||
}
|
||||
}
|
||||
|
||||
/// Appends the given node to the tree, returning its index.
|
||||
fn push_node(&mut self, node: TreeNode) -> Result<TreeIndex, LargeDataset> {
|
||||
let res = self.tree.len().try_into()?;
|
||||
self.tree.push(node);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Pushes the prefix to the no-data list and returns the data index.
|
||||
fn push_no_data(&mut self, prefix: P) -> Result<DataIndex, LargeDataset> {
|
||||
let res = DataIndex::no_data(self.no_data.len())?;
|
||||
self.no_data.push(prefix);
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ RoutePrefix ---------------------------------------------------
|
||||
|
||||
/// The implementatin of `Default` must return the slash zero prefix.
|
||||
pub trait RoutePrefix: Clone + Copy + Default + fmt::Debug + Eq + Ord {
|
||||
/// Returns whether this prefix covers the given prefix.
|
||||
fn covers(self, other: Self) -> bool;
|
||||
|
||||
/// Returns the closest ancestor of the two prefixes.
|
||||
fn closest_ancestor(self, other: Self) -> Self;
|
||||
|
||||
/// Returns the address length of the prefix.
|
||||
fn addr_len(self) -> u8;
|
||||
|
||||
/// Returns the value of the `idx`th bit of the prefix.
|
||||
///
|
||||
/// Bit 0 is the leftmost bit.
|
||||
fn bit(self, idx: u8) -> bool;
|
||||
|
||||
/// Converts the prefix into a typed prefix.
|
||||
fn into_typed_prefix(self) -> TypedPrefix;
|
||||
}
|
||||
|
||||
impl RoutePrefix for Ipv4Prefix {
|
||||
fn covers(self, other: Self) -> bool {
|
||||
if self.addr_len() > other.addr_len() {
|
||||
return false
|
||||
}
|
||||
if self.addr_len() == 32 {
|
||||
return self.addr() == other.addr()
|
||||
}
|
||||
|
||||
self.addr().to_bits()
|
||||
== other.addr().to_bits() & !(u32::MAX >> self.addr_len())
|
||||
}
|
||||
|
||||
fn closest_ancestor(self, other: Self) -> Self {
|
||||
self.resize(
|
||||
cmp::min(
|
||||
(self.addr().to_bits() ^ other.addr().to_bits())
|
||||
.leading_zeros() as u8,
|
||||
cmp::min(self.addr_len(), other.addr_len())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn addr_len(self) -> u8 {
|
||||
self.addr_len()
|
||||
}
|
||||
|
||||
fn bit(self, idx: u8) -> bool {
|
||||
let Some(mask) = 0x8000_0000u32.checked_shr(idx.into()) else {
|
||||
return false
|
||||
};
|
||||
(self.addr().to_bits() & mask) != 0
|
||||
}
|
||||
|
||||
fn into_typed_prefix(self) -> TypedPrefix {
|
||||
TypedPrefix::V4(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl RoutePrefix for Ipv6Prefix {
|
||||
fn covers(self, other: Self) -> bool {
|
||||
if self.addr_len() > other.addr_len() {
|
||||
return false
|
||||
}
|
||||
if self.addr_len() == 128 {
|
||||
return self.addr() == other.addr()
|
||||
}
|
||||
|
||||
self.addr().to_bits()
|
||||
== other.addr().to_bits() & !(u128::MAX >> self.addr_len())
|
||||
}
|
||||
|
||||
fn closest_ancestor(self, other: Self) -> Self {
|
||||
self.resize(
|
||||
cmp::min(
|
||||
(self.addr().to_bits() ^ other.addr().to_bits())
|
||||
.leading_zeros() as u8,
|
||||
cmp::min(self.addr_len(), other.addr_len())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn addr_len(self) -> u8 {
|
||||
self.addr_len()
|
||||
}
|
||||
|
||||
fn bit(self, idx: u8) -> bool {
|
||||
let Some(mask) = const { 1u128 << 127 }.checked_shr(idx.into()) else {
|
||||
return false
|
||||
};
|
||||
(self.addr().to_bits() & mask) != 0
|
||||
}
|
||||
|
||||
fn into_typed_prefix(self) -> TypedPrefix {
|
||||
TypedPrefix::V6(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ TreeIndex ------------------------------------------------------
|
||||
|
||||
/// The optional index of a tree node.
|
||||
///
|
||||
/// The index is kept as a u32 and uses `u32::MAX` as the sentinel for `None`.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct TreeIndex(u32);
|
||||
|
||||
impl TreeIndex {
|
||||
/// Returns the tree index for “none.”
|
||||
const fn none() -> Self {
|
||||
Self(u32::MAX)
|
||||
}
|
||||
|
||||
/// Converts the tree index into an optional usize.
|
||||
fn into_usize(self) -> Option<usize> {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TreeIndex {
|
||||
fn default() -> Self {
|
||||
Self::none()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Option<usize>> for TreeIndex {
|
||||
type Error = LargeDataset;
|
||||
|
||||
fn try_from(src: Option<usize>) -> Result<Self, Self::Error> {
|
||||
match src {
|
||||
Some(src) => {
|
||||
match u32::try_from(src) {
|
||||
Ok(src) if src == u32::MAX => Err(LargeDataset(())),
|
||||
Ok(src) => Ok(Self(src)),
|
||||
Err(_) => Err(LargeDataset(())),
|
||||
}
|
||||
}
|
||||
None => Ok(Self(u32::MAX))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<usize> for TreeIndex {
|
||||
type Error = LargeDataset;
|
||||
|
||||
fn try_from(src: usize) -> Result<Self, Self::Error> {
|
||||
Some(src).try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TreeIndex> for Option<usize> {
|
||||
fn from(src: TreeIndex) -> Self {
|
||||
if src.0 == u32::MAX {
|
||||
None
|
||||
}
|
||||
else {
|
||||
Some(src.0 as usize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ DataIndex -----------------------------------------------------
|
||||
|
||||
/// The index of a data item.
|
||||
///
|
||||
/// This may either be an index into the data vec or an index into the
|
||||
/// non-data vec.
|
||||
///
|
||||
/// The index is kept as an u32. If the left-most bit is set, the
|
||||
/// remaining bits are a data index. If it isn’t, it is a no-data index.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct DataIndex(u32);
|
||||
|
||||
impl DataIndex {
|
||||
/// Returns the 31 bit value for the given usize.
|
||||
fn usize_to_u31(idx: usize) -> Result<u32, LargeDataset> {
|
||||
match u32::try_from(idx) {
|
||||
Ok(idx) if idx & 0x8000_0000 != 0 => Err(LargeDataset(())),
|
||||
Ok(idx) => Ok(idx),
|
||||
Err(_) => Err(LargeDataset(())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the index for the given index into the data set.
|
||||
fn data(idx: usize) -> Result<Self, LargeDataset> {
|
||||
Ok(Self(Self::usize_to_u31(idx)? | 0x8000_0000))
|
||||
}
|
||||
|
||||
/// Returns the index for the given index into the no-data set.
|
||||
fn no_data(idx: usize) -> Result<Self, LargeDataset> {
|
||||
Ok(Self(Self::usize_to_u31(idx)?))
|
||||
}
|
||||
|
||||
/// Converts the index into a usize index.
|
||||
///
|
||||
/// Returns `Ok(_)` if the index is into the data set and `Err(_)` if the
|
||||
/// index is into the no-data set.
|
||||
fn into_data(self) -> Result<usize, usize> {
|
||||
if self.0 & 0x8000_0000 != 0 {
|
||||
Ok((self.0 & 0x7FFF_FFFF) as usize)
|
||||
}
|
||||
else {
|
||||
Err(self.0 as usize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ TreeNode ------------------------------------------------------
|
||||
|
||||
/// A node in the radix tree.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct TreeNode {
|
||||
/// The index of the data item referred to by this node.
|
||||
///
|
||||
/// This is an optional index into the data slice.
|
||||
data: DataIndex,
|
||||
|
||||
/// The index of the left child tree node.
|
||||
///
|
||||
/// This is an optional index into the same tree slice.
|
||||
///
|
||||
/// The left child is the prefix with at least one more bit where the
|
||||
/// next bit is 0.
|
||||
left: TreeIndex,
|
||||
|
||||
/// The right child tree node.
|
||||
///
|
||||
/// This is an optional index into the same tree slice.
|
||||
///
|
||||
/// The left child is the prefix with at least one more bit where the
|
||||
/// next bit is 1.
|
||||
right: TreeIndex,
|
||||
}
|
||||
|
||||
impl TreeNode {
|
||||
/// Creates a new node with the data index and no children.
|
||||
fn new(data: DataIndex) -> Self {
|
||||
Self {
|
||||
data,
|
||||
left: TreeIndex::none(),
|
||||
right: TreeIndex::none(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new node with the data index and the given children.
|
||||
fn with_children(
|
||||
data: DataIndex, left: TreeIndex, right: TreeIndex
|
||||
) -> Self {
|
||||
Self { data, left, right }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ RouteOrigin ---------------------------------------------------
|
||||
|
||||
/// A prefix and an origin AS.
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub struct RouteOrigin<P> {
|
||||
/// The address prefix of this route origin.
|
||||
pub prefix: P,
|
||||
|
||||
/// The origin AS of this route origin.
|
||||
pub origin: AsNumber,
|
||||
}
|
||||
|
||||
impl<P: RoutePrefix> From<RouteOrigin<P>> for Announcement {
|
||||
fn from(src: RouteOrigin<P>) -> Self {
|
||||
Self {
|
||||
asn: src.origin,
|
||||
prefix: src.prefix.into_typed_prefix()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ RouteOriginSet ------------------------------------------------
|
||||
|
||||
/// A set of route origins.
|
||||
///
|
||||
/// This is a thin wrapper around a non-empty slice of [`RouteOrigin<P>']
|
||||
/// with the same prefix, allowing access to the prefix with unwrapping and
|
||||
/// such.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct RouteOriginSet<'a, P> {
|
||||
/// The underlying slice.
|
||||
slice: &'a [RouteOrigin<P>],
|
||||
}
|
||||
|
||||
impl<'a, P: RoutePrefix> RouteOriginSet<'a, P> {
|
||||
/// Creates a new value from a non-empty slice.
|
||||
fn new(slice: &'a [RouteOrigin<P>]) -> Self {
|
||||
debug_assert!(!slice.is_empty());
|
||||
Self { slice }
|
||||
}
|
||||
|
||||
/// Returns the prefix of the set.
|
||||
pub fn prefix(self) -> P {
|
||||
// Safety: self.slice is not empty.
|
||||
self.slice[0].prefix
|
||||
}
|
||||
|
||||
/// Returns an iterator over the individual route origins of the set.
|
||||
pub fn iter(self) -> impl Iterator<Item = RouteOrigin<P>> + 'a {
|
||||
self.slice.iter().copied()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ RouteOriginBox ------------------------------------------------
|
||||
|
||||
/// A boxed slice of sorted `RouteOrgin`s.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RouteOriginBox<P>(Box<[RouteOrigin<P>]>);
|
||||
|
||||
impl<P: RoutePrefix> RouteOriginBox<P> {
|
||||
/// Returns the index of first following entry with a different prefix.
|
||||
fn next_prefix(&self, idx: usize) -> usize {
|
||||
let mut next_idx = idx;
|
||||
loop {
|
||||
next_idx = match next_idx.checked_add(1) {
|
||||
Some(idx) => idx,
|
||||
None => return usize::MAX,
|
||||
};
|
||||
if next_idx >= self.0.len() {
|
||||
return next_idx;
|
||||
}
|
||||
if self.0[next_idx].prefix != self.0[idx].prefix {
|
||||
return next_idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the slice of all entries with the same prefix.
|
||||
fn origin_set(&self, idx: usize) -> Option<RouteOriginSet<'_, P>> {
|
||||
// XXX Check that this will always return non-empty slices or None.
|
||||
let next = cmp::min(self.next_prefix(idx), self.0.len());
|
||||
self.0.get(idx..next).map(RouteOriginSet::new)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> Default for RouteOriginBox<P> {
|
||||
fn default() -> Self {
|
||||
Self(Box::new([]))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//----------- TreeIter -------------------------------------------------------
|
||||
|
||||
/// An iterator over the items in a route origin collection.
|
||||
///
|
||||
/// The iterator goes over the elements in prefix order. That is, prefixes
|
||||
/// with a smaller integer value go first and, if they are the same, those
|
||||
/// with a shorter length go first.
|
||||
///
|
||||
/// The iterator returns non-empty slices of route origins with the same
|
||||
/// prefix.
|
||||
pub struct TreeIter<'a, P> {
|
||||
/// A reference to the collection we iterate over.
|
||||
collection: &'a RouteOriginCollection<P>,
|
||||
|
||||
/// The stack for recursion.
|
||||
///
|
||||
/// The last item is the node we need to process in this call to `next`.
|
||||
tree_idx_stack: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<'a, P: RoutePrefix> TreeIter<'a, P> {
|
||||
#[cfg(test)]
|
||||
fn new(collection: &'a RouteOriginCollection<P>) -> Self {
|
||||
Self {
|
||||
collection,
|
||||
tree_idx_stack: match collection.tree_root_idx.into_usize() {
|
||||
Some(idx) => vec![idx],
|
||||
None => Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new iterator starting at the given prefix.
|
||||
fn more_specific(
|
||||
collection: &'a RouteOriginCollection<P>,
|
||||
root_prefix: P
|
||||
) -> Self {
|
||||
let mut tree_idx = collection.tree_root_idx;
|
||||
while let Some(node) = collection.get_tree_node(tree_idx) {
|
||||
let Some(prefix) = collection.get_data_prefix(node.data) else {
|
||||
break;
|
||||
};
|
||||
if prefix.addr_len() >= root_prefix.addr_len() {
|
||||
let Some(tree_idx) = tree_idx.into_usize() else {
|
||||
break
|
||||
};
|
||||
return Self {
|
||||
collection,
|
||||
tree_idx_stack: vec![tree_idx],
|
||||
}
|
||||
}
|
||||
if !root_prefix.bit(prefix.addr_len()) {
|
||||
tree_idx = node.left
|
||||
}
|
||||
else {
|
||||
tree_idx = node.right
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
collection,
|
||||
tree_idx_stack: Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, P: RoutePrefix> Iterator for TreeIter<'a, P> {
|
||||
type Item = RouteOriginSet<'a, P>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// We iterate node itself first, then left, then right.
|
||||
loop {
|
||||
let node_idx = *self.tree_idx_stack.last()?;
|
||||
let node = self.collection.tree.get(node_idx)?;
|
||||
self.tree_idx_stack.pop();
|
||||
|
||||
if let Some(idx) = node.right.into_usize() {
|
||||
self.tree_idx_stack.push(idx);
|
||||
}
|
||||
if let Some(idx) = node.left.into_usize() {
|
||||
self.tree_idx_stack.push(idx);
|
||||
}
|
||||
|
||||
if let Ok(idx) = node.data.into_data() {
|
||||
return self.collection.data.origin_set(idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//=========== Error Types ====================================================
|
||||
|
||||
//------------ RisWhoisError ------------------------------------------------
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RisWhoisError {
|
||||
uri: String,
|
||||
err: io::Error,
|
||||
}
|
||||
|
||||
impl RisWhoisError {
|
||||
fn new(uri: &str, err: io::Error) -> Self {
|
||||
Self { uri: uri.into(), err }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RisWhoisError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f,
|
||||
"Failed to download RISwhois file `{}`: {}",
|
||||
self.uri, self.err
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//----------- LargeDataset -----------------------------------------------------
|
||||
|
||||
/// The dataset is too large to fit into the route origin collection.
|
||||
#[derive(Debug)]
|
||||
pub struct LargeDataset(());
|
||||
|
||||
impl fmt::Display for LargeDataset {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt.write_str("RISwhois dataset too large")
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for LargeDataset { }
|
||||
|
||||
//------------ Tests --------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_bgp_ris_dumps() {
|
||||
let v4 = RisWhoisLoader::parse_data(include_bytes!(
|
||||
"../../../test-resources/bgp/riswhoisdump.IPv4"
|
||||
).as_ref()).unwrap();
|
||||
let v6 = RisWhoisLoader::parse_data(include_bytes!(
|
||||
"../../../test-resources/bgp/riswhoisdump.IPv6"
|
||||
).as_ref()).unwrap();
|
||||
let ris = RisWhois { v4, v6 };
|
||||
|
||||
let v4 = TreeIter::new(&ris.v4).map(|item| {
|
||||
for origin in item.iter() {
|
||||
assert_eq!(origin.prefix, item.prefix());
|
||||
}
|
||||
item.prefix()
|
||||
}).collect::<Vec<_>>();
|
||||
for item in v4.windows(2) {
|
||||
assert!(item[0] < item[1])
|
||||
}
|
||||
let v6 = TreeIter::new(&ris.v6).map(|item| {
|
||||
for origin in item.iter() {
|
||||
assert_eq!(origin.prefix, item.prefix());
|
||||
}
|
||||
item.prefix()
|
||||
}).collect::<Vec<_>>();
|
||||
for item in v6.windows(2) {
|
||||
assert!(item[0] < item[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::collections::HashMap;
|
||||
use std::ops::Range;
|
||||
use std::str::FromStr;
|
||||
use intervaltree::IntervalTree;
|
||||
use rpki::repository::resources::{Addr, AddressRange, ResourceSet};
|
||||
use rpki::repository::resources::{Addr, AddressRange, Prefix, ResourceSet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -100,6 +100,10 @@ impl BgpAnalyser {
|
||||
// Now get all the necessary data from BGP API.
|
||||
//
|
||||
// Return early if this failed.
|
||||
//
|
||||
// If this succeeds, `scoped_announcements` will contain all
|
||||
// announcements that overlap any of the IP address resources we
|
||||
// are considering.
|
||||
let scope = IpRange::from_resource_set(
|
||||
match &limited_scope {
|
||||
Some(limit) => limit,
|
||||
@@ -124,27 +128,33 @@ impl BgpAnalyser {
|
||||
}
|
||||
}
|
||||
|
||||
// Now create a prefix tree for all the configured ROAs: `roa_tree`.
|
||||
let roa_tree = IpRangeStore::create(
|
||||
roas_held.iter().map(|configured| {
|
||||
let payload = configured.roa_configuration.payload;
|
||||
(payload.prefix.into(), payload)
|
||||
})
|
||||
);
|
||||
|
||||
// Now go over all announcements and determine their ROV status from
|
||||
// our ROAs. Turn that into a prefix tree: `validated_tree`.
|
||||
let validated: Vec<ValidatedAnnouncement> = scoped_announcements
|
||||
.into_iter()
|
||||
.map(|a| roa_tree.validate_announcement(a))
|
||||
.collect();
|
||||
|
||||
// Check all ROAs.. and report ROA state in relation to validated
|
||||
// announcements
|
||||
let validated_tree = IpRangeStore::create(
|
||||
validated.iter().map(|v| (v.announcement.prefix.into(), v.clone()))
|
||||
);
|
||||
|
||||
// Now we go over each individual configured ROA and check how it
|
||||
// influenced the validated tree.
|
||||
for roa in roas_held {
|
||||
// Get all announcements covered by the ROA.
|
||||
let covered = validated_tree.matching_or_more_specific(
|
||||
roa.roa_configuration.payload.prefix
|
||||
);
|
||||
|
||||
// Get all other ROAs that cover the prefix of this ROA.
|
||||
let other_roas_covering_this_prefix: Vec<_> = roa_tree
|
||||
.matching_or_less_specific(
|
||||
roa.roa_configuration.payload.prefix
|
||||
@@ -154,6 +164,7 @@ impl BgpAnalyser {
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Get all ROAs that include this ROA.
|
||||
let other_roas_including_this_definition: Vec<_> =
|
||||
other_roas_covering_this_prefix
|
||||
.iter()
|
||||
@@ -414,7 +425,7 @@ impl BgpAnalyser {
|
||||
// When testing, the "test" URL is special. Also, unwrapping is
|
||||
// fine.
|
||||
let value = serde_json::from_str::<Value>(include_str!(
|
||||
"../../test-resources/bgp/bgp-api.json")
|
||||
"../../../test-resources/bgp/bgp-api.json")
|
||||
).unwrap();
|
||||
let Value::Object(mut value) = value else {
|
||||
panic!("not an object")
|
||||
@@ -623,13 +634,13 @@ impl From<TypedPrefix> for IpRange {
|
||||
fn from(tp: TypedPrefix) -> Self {
|
||||
match tp {
|
||||
TypedPrefix::V4(pfx) => {
|
||||
let (min, max) = pfx.as_ref().range();
|
||||
let (min, max) = Prefix::from(pfx).range();
|
||||
let start = min.to_v4().to_ipv6_mapped().into();
|
||||
let end = max.to_v4().to_ipv6_mapped().into();
|
||||
IpRange(Range { start, end })
|
||||
}
|
||||
TypedPrefix::V6(pfx) => {
|
||||
let (min, max) = pfx.as_ref().range();
|
||||
let (min, max) = Prefix::from(pfx).range();
|
||||
let start = min.to_v6().into();
|
||||
let end = max.to_v6().into();
|
||||
IpRange(Range { start, end })
|
||||
@@ -876,7 +887,7 @@ mod test {
|
||||
.await;
|
||||
|
||||
let expected: BgpAnalysisReport = serde_json::from_str(include_str!(
|
||||
"../../test-resources/bgp/expected_full_report.json"
|
||||
"../../../test-resources/bgp/expected_full_report.json"
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
@@ -990,7 +1001,7 @@ mod test {
|
||||
|
||||
let expected: BgpAnalysisSuggestion =
|
||||
serde_json::from_str(include_str!(
|
||||
"../../test-resources/bgp/expected_suggestion_some_roas.json"
|
||||
"../../../test-resources/bgp/expected_suggestion_some_roas.json"
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(suggestion_resource_subset, expected);
|
||||
@@ -1000,7 +1011,7 @@ mod test {
|
||||
|
||||
let expected: BgpAnalysisSuggestion =
|
||||
serde_json::from_str(include_str!(
|
||||
"../../test-resources/bgp/expected_suggestion_all_roas.json"
|
||||
"../../../test-resources/bgp/expected_suggestion_all_roas.json"
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
+20
-27
@@ -151,13 +151,7 @@ impl KrillManager {
|
||||
.await?,
|
||||
);
|
||||
|
||||
let bgp_analyser = Arc::new(BgpAnalyser::new(
|
||||
config.bgp_api_enabled,
|
||||
config.bgp_api_uri.clone(),
|
||||
config.bgp_api_cache_duration.to_std().unwrap_or(
|
||||
std::time::Duration::from_secs(0)
|
||||
)
|
||||
));
|
||||
let bgp_analyser = Arc::new(BgpAnalyser::new(&config));
|
||||
|
||||
// When multi-node set ups with a shared queue are
|
||||
// supported then we can no longer safely reschedule
|
||||
@@ -277,6 +271,7 @@ impl KrillManager {
|
||||
self.mq.clone(),
|
||||
self.ca_manager.clone(),
|
||||
self.repo_manager.clone(),
|
||||
self.bgp_analyser.clone(),
|
||||
self.config.clone(),
|
||||
self.system_actor.clone(),
|
||||
)
|
||||
@@ -623,7 +618,7 @@ impl KrillManager {
|
||||
|
||||
/// # Stats and status of CAS
|
||||
impl KrillManager {
|
||||
pub async fn cas_stats(
|
||||
pub fn cas_stats(
|
||||
&self,
|
||||
) -> KrillResult<HashMap<CaHandle, CertAuthStats>> {
|
||||
let mut res = HashMap::new();
|
||||
@@ -639,10 +634,11 @@ impl KrillManager {
|
||||
|| ca.handle().as_str() == "testbed"
|
||||
{
|
||||
BgpAnalysisReport::new(vec![])
|
||||
} else {
|
||||
self.bgp_analyser
|
||||
.analyse(roas.as_slice(), &ca.all_resources(), None)
|
||||
.await
|
||||
}
|
||||
else {
|
||||
self.bgp_analyser.analyse(
|
||||
roas.as_slice(), &ca.all_resources(), None
|
||||
)
|
||||
};
|
||||
|
||||
res.insert(
|
||||
@@ -1146,20 +1142,19 @@ impl KrillManager {
|
||||
Ok(ca.configured_roas())
|
||||
}
|
||||
|
||||
pub async fn ca_routes_bgp_analysis(
|
||||
pub fn ca_routes_bgp_analysis(
|
||||
&self,
|
||||
handle: &CaHandle,
|
||||
) -> KrillResult<BgpAnalysisReport> {
|
||||
let ca = self.ca_manager.get_ca(handle)?;
|
||||
let definitions = ca.configured_roas();
|
||||
let resources_held = ca.all_resources();
|
||||
Ok(self
|
||||
.bgp_analyser
|
||||
.analyse(definitions.as_slice(), &resources_held, None)
|
||||
.await)
|
||||
Ok(self.bgp_analyser.analyse(
|
||||
definitions.as_slice(), &resources_held, None
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn ca_routes_bgp_dry_run(
|
||||
pub fn ca_routes_bgp_dry_run(
|
||||
&self,
|
||||
handle: &CaHandle,
|
||||
mut updates: RoaConfigurationUpdates,
|
||||
@@ -1175,13 +1170,12 @@ impl KrillManager {
|
||||
let configured_roas =
|
||||
ca.configured_roas_for_configs(would_be_configurations);
|
||||
|
||||
Ok(self
|
||||
.bgp_analyser
|
||||
.analyse(&configured_roas, &resources_held, limit)
|
||||
.await)
|
||||
Ok(self.bgp_analyser.analyse(
|
||||
&configured_roas, &resources_held, limit
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn ca_routes_bgp_suggest(
|
||||
pub fn ca_routes_bgp_suggest(
|
||||
&self,
|
||||
handle: &CaHandle,
|
||||
limit: Option<ResourceSet>,
|
||||
@@ -1190,10 +1184,9 @@ impl KrillManager {
|
||||
let configured_roas = ca.configured_roas();
|
||||
let resources_held = ca.all_resources();
|
||||
|
||||
Ok(self
|
||||
.bgp_analyser
|
||||
.suggest(configured_roas.as_slice(), &resources_held, limit)
|
||||
.await)
|
||||
Ok(self.bgp_analyser.suggest(
|
||||
configured_roas.as_slice(), &resources_held, limit
|
||||
))
|
||||
}
|
||||
|
||||
/// Re-issue ROA objects so that they will use short subjects (see issue
|
||||
|
||||
+32
-1
@@ -31,6 +31,7 @@ use crate::{
|
||||
config::Config,
|
||||
server::{
|
||||
ca::{CaManager, CertAuth},
|
||||
bgp::BgpAnalyser,
|
||||
mq::{
|
||||
in_hours, in_minutes, in_seconds, in_weeks, now, Task, TaskQueue,
|
||||
},
|
||||
@@ -45,6 +46,7 @@ pub struct Scheduler {
|
||||
tasks: Arc<TaskQueue>,
|
||||
ca_manager: Arc<CaManager>,
|
||||
repo_manager: Arc<RepositoryManager>,
|
||||
bgp_analyser: Arc<BgpAnalyser>,
|
||||
config: Arc<Config>,
|
||||
system_actor: Actor,
|
||||
started: Timestamp,
|
||||
@@ -55,6 +57,7 @@ impl Scheduler {
|
||||
tasks: Arc<TaskQueue>,
|
||||
ca_manager: Arc<CaManager>,
|
||||
repo_manager: Arc<RepositoryManager>,
|
||||
bgp_analyser: Arc<BgpAnalyser>,
|
||||
config: Arc<Config>,
|
||||
system_actor: Actor,
|
||||
) -> Self {
|
||||
@@ -62,6 +65,7 @@ impl Scheduler {
|
||||
tasks,
|
||||
ca_manager,
|
||||
repo_manager,
|
||||
bgp_analyser,
|
||||
config,
|
||||
system_actor,
|
||||
started: Timestamp::now(),
|
||||
@@ -190,7 +194,11 @@ impl Scheduler {
|
||||
.await
|
||||
}
|
||||
|
||||
Task::SweepLoginCache | Task::RefreshAnnouncementsInfo => {
|
||||
Task::RefreshAnnouncementsInfo => {
|
||||
self.announcements_refresh().await
|
||||
}
|
||||
|
||||
Task::SweepLoginCache => {
|
||||
// Don’t do anything. These are deprecated.
|
||||
Ok(TaskResult::Done)
|
||||
}
|
||||
@@ -298,6 +306,15 @@ impl Scheduler {
|
||||
.schedule_missing(Task::RenewObjectsIfNeeded, now())
|
||||
.map_err(FatalError)?;
|
||||
|
||||
// BGP announcement info is only kept in-memory, so it
|
||||
// is lost after a restart, so schedule refreshing this
|
||||
// immediately.
|
||||
if self.config.bgp_riswhois_enabled {
|
||||
self.tasks
|
||||
.schedule(Task::RefreshAnnouncementsInfo, now())
|
||||
.map_err(FatalError)?;
|
||||
}
|
||||
|
||||
// Plan updating snapshots soon after a restart.
|
||||
// This also ensures that this task gets triggered in long
|
||||
// running tests, such as functional_parent_child.rs.
|
||||
@@ -485,6 +502,20 @@ impl Scheduler {
|
||||
))
|
||||
}
|
||||
|
||||
/// Update announcement info
|
||||
async fn announcements_refresh(&self) -> Result<TaskResult, FatalError> {
|
||||
if let Err(e) = self.bgp_analyser.update().await {
|
||||
error!("Failed to update BGP announcements: {}", e)
|
||||
}
|
||||
|
||||
// check again in 10 minutes, note.. this is a no-op in case the
|
||||
// actual update was less then 1 hour ago.
|
||||
// See BGP_RIS_REFRESH_MINUTES constant.
|
||||
Ok(TaskResult::FollowUp(
|
||||
Task::RefreshAnnouncementsInfo, in_minutes(10)
|
||||
))
|
||||
}
|
||||
|
||||
/// Let CAs that need it re-issue signed objects
|
||||
async fn renew_objects_if_needed(
|
||||
&self,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+9
-6
@@ -164,9 +164,11 @@ impl TestConfig {
|
||||
let post_protocol_msg_timeout_seconds =
|
||||
ConfigDefaults::post_protocol_msg_timeout_seconds();
|
||||
|
||||
let bgp_api_enabled = false;
|
||||
let bgp_api_uri = ConfigDefaults::bgp_api_uri();
|
||||
let bgp_api_cache_duration = ConfigDefaults::bgp_api_cache_duration();
|
||||
let bgp_riswhois_enabled = false;
|
||||
let bgp_riswhois_v4_uri = ConfigDefaults::bgp_riswhois_v4_uri();
|
||||
let bgp_riswhois_v6_uri = ConfigDefaults::bgp_riswhois_v6_uri();
|
||||
let bgp_riswhois_refresh_interval
|
||||
= ConfigDefaults::bgp_riswhois_refresh_interval();
|
||||
|
||||
let roa_aggregate_threshold = 3;
|
||||
let roa_deaggregate_threshold = 2;
|
||||
@@ -282,9 +284,10 @@ impl TestConfig {
|
||||
post_limit_rfc6492,
|
||||
rfc6492_log_dir: None,
|
||||
post_protocol_msg_timeout_seconds,
|
||||
bgp_api_enabled,
|
||||
bgp_api_uri,
|
||||
bgp_api_cache_duration,
|
||||
bgp_riswhois_enabled,
|
||||
bgp_riswhois_v4_uri,
|
||||
bgp_riswhois_v6_uri,
|
||||
bgp_riswhois_refresh_interval,
|
||||
roa_aggregate_threshold,
|
||||
roa_deaggregate_threshold,
|
||||
issuance_timing,
|
||||
|
||||
Reference in New Issue
Block a user