diff --git a/src/cli/client.rs b/src/cli/client.rs index 45d5f097..81cb98b8 100644 --- a/src/cli/client.rs +++ b/src/cli/client.rs @@ -13,7 +13,6 @@ use crate::commons::api::{ AllCertAuthIssues, CaRepoDetails, CertAuthIssues, ChildCaInfo, CurrentRepoState, ParentCaContact, PublisherDetails, PublisherList, Token, }; -use crate::commons::bgp::{RoaSummary, RoaTable}; use crate::commons::remote::rfc8183; use crate::commons::util::httpclient; use crate::constants::KRILL_CLI_API_ENV; @@ -212,17 +211,16 @@ impl KrillClient { Ok(ApiResponse::Empty) } - CaCommand::RouteAuthorizationsBgpDetails(handle) => { - let uri = format!("api/v1/cas/{}/routes/bgp", handle); - let roa_table = self.get_json(&uri).await?; - Ok(ApiResponse::RouteAuthorizationsBgpDetails(roa_table)) + CaCommand::BgpAnalysisFull(handle) => { + let uri = format!("api/v1/cas/{}/routes/analysis/full", handle); + let report = self.get_json(&uri).await?; + Ok(ApiResponse::BgpAnalysisFull(report)) } - CaCommand::RouteAuthorizationsBgpSummary(handle) => { - let uri = format!("api/v1/cas/{}/routes/bgp", handle); - let roa_table: RoaTable = self.get_json(&uri).await?; - let summary: RoaSummary = roa_table.into(); - Ok(ApiResponse::RouteAuthorizationsBgpSummary(summary)) + CaCommand::BgpAnalysisAnnouncements(handle) => { + let uri = format!("api/v1/cas/{}/routes/analysis/announcements", handle); + let report = self.get_json(&uri).await?; + Ok(ApiResponse::BgpAnalysisAnnouncements(report)) } CaCommand::Show(handle) => { diff --git a/src/cli/options.rs b/src/cli/options.rs index 0f670ba4..f62d4ecf 100644 --- a/src/cli/options.rs +++ b/src/cli/options.rs @@ -607,19 +607,29 @@ impl Options { app.subcommand(sub) } + fn make_cas_routes_bgp_full_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = SubCommand::with_name("full").about("Show full report."); + + sub = Self::add_general_args(sub); + sub = Self::add_my_ca_arg(sub); + app.subcommand(sub) + } + + fn make_cas_routes_bgp_announcements_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = + SubCommand::with_name("announcements").about("Show announcement centric report."); + + sub = Self::add_general_args(sub); + sub = Self::add_my_ca_arg(sub); + app.subcommand(sub) + } + fn make_cas_routes_bgp_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { let mut sub = SubCommand::with_name("bgp") .about("Show current authorizations in relation to known announcements."); - sub = Self::add_general_args(sub); - sub = Self::add_my_ca_arg(sub); - - sub = sub.arg( - Arg::with_name("full") - .long("full") - .help("Show detailed view instead of summary") - .required(false), - ); + sub = Self::make_cas_routes_bgp_full_sc(sub); + sub = Self::make_cas_routes_bgp_announcements_sc(sub); app.subcommand(sub) } @@ -1305,17 +1315,32 @@ impl Options { Ok(Options::make(general_args, command)) } - fn parse_matches_cas_routes_bgp(matches: &ArgMatches) -> Result { + fn parse_matches_cas_routes_bgp_full(matches: &ArgMatches) -> Result { let general_args = GeneralArgs::from_matches(matches)?; let my_ca = Self::parse_my_ca(matches)?; + Ok(Options::make( + general_args, + Command::CertAuth(CaCommand::BgpAnalysisFull(my_ca)), + )) + } - let command = if matches.is_present("full") { - Command::CertAuth(CaCommand::RouteAuthorizationsBgpDetails(my_ca)) + fn parse_matches_cas_routes_bgp_announcements(matches: &ArgMatches) -> Result { + let general_args = GeneralArgs::from_matches(matches)?; + let my_ca = Self::parse_my_ca(matches)?; + Ok(Options::make( + general_args, + Command::CertAuth(CaCommand::BgpAnalysisAnnouncements(my_ca)), + )) + } + + fn parse_matches_cas_routes_bgp(matches: &ArgMatches) -> Result { + if let Some(m) = matches.subcommand_matches("full") { + Self::parse_matches_cas_routes_bgp_full(m) + } else if let Some(m) = matches.subcommand_matches("announcements") { + Self::parse_matches_cas_routes_bgp_announcements(m) } else { - Command::CertAuth(CaCommand::RouteAuthorizationsBgpSummary(my_ca)) - }; - - Ok(Options::make(general_args, command)) + Err(Error::UnrecognisedSubCommand) + } } fn parse_matches_cas_routes(matches: &ArgMatches) -> Result { @@ -1659,10 +1684,10 @@ pub enum CaCommand { RouteAuthorizationsUpdate(Handle, RoaDefinitionUpdates), #[display(fmt = "Show detailed ROA vs BGP analysis for ca: '{}'", _0)] - RouteAuthorizationsBgpDetails(Handle), + BgpAnalysisFull(Handle), #[display(fmt = "Show summary of ROA vs BGP analysis for ca: '{}'", _0)] - RouteAuthorizationsBgpSummary(Handle), + BgpAnalysisAnnouncements(Handle), // Show details for this CA #[display(fmt = "Show details for ca: '{}'", _0)] diff --git a/src/cli/report.rs b/src/cli/report.rs index a5b1853d..2ddef0f2 100644 --- a/src/cli/report.rs +++ b/src/cli/report.rs @@ -11,7 +11,7 @@ use crate::commons::api::{ ParentCaContact, PublisherDetails, PublisherList, RepositoryContact, RoaDefinition, ServerInfo, StoredEffect, }; -use crate::commons::bgp::{RoaSummary, RoaTable}; +use crate::commons::bgp::{AnnouncementReport, BgpAnalysisReport}; use crate::commons::eventsourcing::WithStorableDetails; use crate::commons::remote::api::ClientInfo; use crate::commons::remote::rfc8183; @@ -31,8 +31,8 @@ pub enum ApiResponse { CertAuthAction(CaCommandDetails), CertAuths(CertAuthList), RouteAuthorizations(Vec), - RouteAuthorizationsBgpDetails(RoaTable), - RouteAuthorizationsBgpSummary(RoaSummary), + BgpAnalysisFull(BgpAnalysisReport), + BgpAnalysisAnnouncements(AnnouncementReport), ParentCaContact(ParentCaContact), @@ -72,10 +72,8 @@ impl ApiResponse { ApiResponse::CertAuthIssues(issues) => Ok(Some(issues.report(fmt)?)), ApiResponse::AllCertAuthIssues(issues) => Ok(Some(issues.report(fmt)?)), ApiResponse::RouteAuthorizations(auths) => Ok(Some(auths.report(fmt)?)), - ApiResponse::RouteAuthorizationsBgpDetails(table) => Ok(Some(table.report(fmt)?)), - ApiResponse::RouteAuthorizationsBgpSummary(summary) => { - Ok(Some(summary.report(fmt)?)) - } + ApiResponse::BgpAnalysisFull(table) => Ok(Some(table.report(fmt)?)), + ApiResponse::BgpAnalysisAnnouncements(summary) => Ok(Some(summary.report(fmt)?)), ApiResponse::ParentCaContact(contact) => Ok(Some(contact.report(fmt)?)), ApiResponse::ChildInfo(info) => Ok(Some(info.report(fmt)?)), ApiResponse::PublisherList(list) => Ok(Some(list.report(fmt)?)), @@ -414,13 +412,13 @@ impl Report for Vec { } } -impl Report for RoaTable { +impl Report for BgpAnalysisReport { fn text(&self) -> Result { Ok(self.to_string()) } } -impl Report for RoaSummary { +impl Report for AnnouncementReport { fn text(&self) -> Result { Ok(self.to_string()) } diff --git a/src/commons/bgp/analyser.rs b/src/commons/bgp/analyser.rs index af5e8eff..87102b17 100644 --- a/src/commons/bgp/analyser.rs +++ b/src/commons/bgp/analyser.rs @@ -7,11 +7,13 @@ use rpki::x509::Time; use crate::commons::api::{ResourceSet, RoaDefinition}; use crate::commons::bgp::{ make_roa_tree, make_validated_announcement_tree, Announcement, AnnouncementValidity, - Announcements, IpRange, RisDumpError, RisDumpLoader, RoaTable, RoaTableEntry, + Announcements, BgpAnalysisEntry, BgpAnalysisReport, IpRange, RisDumpError, RisDumpLoader, ValidatedAnnouncement, }; use crate::constants::BGP_RIS_REFRESH_MINUTES; +//------------ BgpAnalyser ------------------------------------------------- + /// This type helps analyse ROAs vs BGP and vice versa. pub struct BgpAnalyser { dumploader: Option, @@ -57,14 +59,14 @@ impl BgpAnalyser { } } - pub fn analyse(&self, roas: &[RoaDefinition], scope: &ResourceSet) -> RoaTable { + pub fn analyse(&self, roas: &[RoaDefinition], scope: &ResourceSet) -> BgpAnalysisReport { let seen = self.seen.read().unwrap(); let mut entries = vec![]; if seen.last_updated().is_none() { // nothing to analyse, just push all ROAs as 'no announcement info' for roa in roas { - entries.push(RoaTableEntry::roa_no_announcement_info(roa.clone())); + entries.push(BgpAnalysisEntry::roa_no_announcement_info(roa.clone())); } } else { let roa_tree = make_roa_tree(roas); @@ -91,7 +93,7 @@ impl BgpAnalyser { for roa in roas { let covered = validated_tree.matching_or_more_specific(&roa.prefix()); if covered.is_empty() { - entries.push(RoaTableEntry::roa_stale(roa.clone())) + entries.push(BgpAnalysisEntry::roa_stale(roa.clone())) } else { let allows: Vec = covered .iter() @@ -113,9 +115,9 @@ impl BgpAnalyser { .collect(); if allows.is_empty() { - entries.push(RoaTableEntry::roa_disallowing(roa.clone(), disallows)); + entries.push(BgpAnalysisEntry::roa_disallowing(roa.clone(), disallows)); } else { - entries.push(RoaTableEntry::roa_authorizing( + entries.push(BgpAnalysisEntry::roa_authorizing( roa.clone(), allows, disallows, @@ -126,28 +128,33 @@ impl BgpAnalyser { // Loop over all validated announcements and report for v in validated.into_iter() { - let (announcement, validity, _, invalidating_roas) = v.unpack(); + let (announcement, validity, allowed_by, invalidating_roas) = v.unpack(); match validity { - AnnouncementValidity::Valid => {} // will show up under ROAs + AnnouncementValidity::Valid => { + entries.push(BgpAnalysisEntry::announcement_valid( + announcement, + allowed_by.unwrap(), // always set for valid announcements + )) + } AnnouncementValidity::InvalidLength => { - entries.push(RoaTableEntry::announcement_invalid_length( + entries.push(BgpAnalysisEntry::announcement_invalid_length( announcement, invalidating_roas, )); } AnnouncementValidity::InvalidAsn => { - entries.push(RoaTableEntry::announcement_invalid_asn( + entries.push(BgpAnalysisEntry::announcement_invalid_asn( announcement, invalidating_roas, )); } AnnouncementValidity::NotFound => { - entries.push(RoaTableEntry::announcement_not_found(announcement)); + entries.push(BgpAnalysisEntry::announcement_not_found(announcement)); } } } } - RoaTable::new(entries) + BgpAnalysisReport::new(entries) } #[cfg(test)] @@ -182,7 +189,7 @@ impl From for BgpAnalyserError { #[cfg(test)] mod tests { - use crate::commons::bgp::RoaTableEntryState; + use crate::commons::bgp::BgpAnalysisState; use crate::test::*; use super::*; @@ -226,14 +233,15 @@ mod tests { ann_not_found, ann_irrelevant, ]); - let table = analyser.analyse(&[roa_authorizing, roa_stale, roa_disallowing], &resources); - let expected_table: RoaTable = serde_json::from_str(include_str!( - "../../../test-resources/bgp/expected_roa_table.json" + let report = analyser.analyse(&[roa_authorizing, roa_stale, roa_disallowing], &resources); + + let expected: BgpAnalysisReport = serde_json::from_str(include_str!( + "../../../test-resources/bgp/expected_bgp_analyis_report.json" )) .unwrap(); - assert_eq!(table, expected_table); + assert_eq!(report, expected); } #[test] @@ -251,7 +259,7 @@ mod tests { let roas_no_info: Vec<&RoaDefinition> = table_entries .iter() - .filter(|e| e.state() == RoaTableEntryState::RoaNoAnnouncementInfo) + .filter(|e| e.state() == BgpAnalysisState::RoaNoAnnouncementInfo) .map(|e| e.definition()) .collect(); diff --git a/src/commons/bgp/announcements.rs b/src/commons/bgp/announcements.rs index 059afc0e..6a6a5ae0 100644 --- a/src/commons/bgp/announcements.rs +++ b/src/commons/bgp/announcements.rs @@ -67,8 +67,8 @@ impl Announcement { ValidatedAnnouncement { announcement: self.clone(), validity: AnnouncementValidity::NotFound, - validating: None, - invalidating: vec![], + authorizing: None, + disallowing: vec![], } } else { let mut invalidating = vec![]; @@ -81,8 +81,8 @@ impl Announcement { return ValidatedAnnouncement { announcement: self.clone(), validity: AnnouncementValidity::Valid, - validating: Some(roa.clone()), - invalidating: vec![], + authorizing: Some(roa.clone()), + disallowing: vec![], }; } else { same_asn_found = true; @@ -100,8 +100,8 @@ impl Announcement { ValidatedAnnouncement { announcement: self.clone(), validity, - validating: None, - invalidating, + authorizing: None, + disallowing: invalidating, } } } @@ -227,8 +227,8 @@ impl Default for Announcements { pub struct ValidatedAnnouncement { announcement: Announcement, validity: AnnouncementValidity, - validating: Option, - invalidating: Vec, + authorizing: Option, + disallowing: Vec, } impl ValidatedAnnouncement { @@ -251,8 +251,8 @@ impl ValidatedAnnouncement { ( self.announcement, self.validity, - self.validating, - self.invalidating, + self.authorizing, + self.disallowing, ) } } diff --git a/src/commons/bgp/mod.rs b/src/commons/bgp/mod.rs index 35773ade..447d3ae5 100644 --- a/src/commons/bgp/mod.rs +++ b/src/commons/bgp/mod.rs @@ -10,5 +10,5 @@ pub use self::iptree::*; mod risdumps; pub use self::risdumps::*; -mod roa_table; -pub use self::roa_table::*; +mod report; +pub use self::report::*; diff --git a/src/commons/bgp/roa_table.rs b/src/commons/bgp/report.rs similarity index 57% rename from src/commons/bgp/roa_table.rs rename to src/commons/bgp/report.rs index ee103b02..7f382d85 100644 --- a/src/commons/bgp/roa_table.rs +++ b/src/commons/bgp/report.rs @@ -5,172 +5,38 @@ use std::fmt; use crate::commons::api::RoaDefinition; use crate::commons::bgp::Announcement; -#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialOrd, PartialEq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum RoaTableEntryState { - RoaAuthorizing, - RoaDisallowing, - RoaStale, - AnnouncementInvalidLength, - AnnouncementInvalidAsn, - AnnouncementNotFound, - RoaNoAnnouncementInfo, -} +//------------ BgpAnalysisReport ------------------------------------------- #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] -pub struct RoaTableEntry { - #[serde(flatten)] - definition: RoaDefinition, - state: RoaTableEntryState, - #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")] - disallowed_by: Vec, - #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")] - authorizes: Vec, - #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")] - disallows: Vec, -} +pub struct BgpAnalysisReport(Vec); -impl RoaTableEntry { - pub fn state(&self) -> RoaTableEntryState { - self.state - } - - pub fn definition(&self) -> &RoaDefinition { - &self.definition - } - - pub fn roa_authorizing( - definition: RoaDefinition, - mut authorizes: Vec, - mut disallows: Vec, - ) -> Self { - authorizes.sort(); - disallows.sort(); - RoaTableEntry { - definition, - state: RoaTableEntryState::RoaAuthorizing, - disallowed_by: vec![], - authorizes, - disallows, - } - } - - pub fn roa_disallowing(definition: RoaDefinition, mut disallows: Vec) -> Self { - disallows.sort(); - RoaTableEntry { - definition, - state: RoaTableEntryState::RoaDisallowing, - disallowed_by: vec![], - authorizes: vec![], - disallows, - } - } - - pub fn roa_stale(definition: RoaDefinition) -> Self { - RoaTableEntry { - definition, - state: RoaTableEntryState::RoaStale, - disallowed_by: vec![], - authorizes: vec![], - disallows: vec![], - } - } - - pub fn roa_no_announcement_info(definition: RoaDefinition) -> Self { - RoaTableEntry { - definition, - state: RoaTableEntryState::RoaNoAnnouncementInfo, - disallowed_by: vec![], - authorizes: vec![], - disallows: vec![], - } - } - - pub fn announcement_invalid_asn( - announcement: Announcement, - mut disallowed_by: Vec, - ) -> Self { - disallowed_by.sort(); - RoaTableEntry { - definition: RoaDefinition::from(announcement), - state: RoaTableEntryState::AnnouncementInvalidAsn, - disallowed_by, - authorizes: vec![], - disallows: vec![], - } - } - - pub fn announcement_invalid_length( - announcement: Announcement, - mut disallowed_by: Vec, - ) -> Self { - disallowed_by.sort(); - RoaTableEntry { - definition: RoaDefinition::from(announcement), - state: RoaTableEntryState::AnnouncementInvalidLength, - disallowed_by, - authorizes: vec![], - disallows: vec![], - } - } - - pub fn announcement_not_found(announcement: Announcement) -> Self { - RoaTableEntry { - definition: RoaDefinition::from(announcement), - state: RoaTableEntryState::AnnouncementNotFound, - disallowed_by: vec![], - authorizes: vec![], - disallows: vec![], - } - } -} - -impl Ord for RoaTableEntry { - fn cmp(&self, other: &Self) -> Ordering { - let mut ordering = self.state.cmp(&other.state); - if ordering == Ordering::Equal { - ordering = self.definition.cmp(&other.definition); - } - ordering - } -} - -impl PartialOrd for RoaTableEntry { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] -pub struct RoaTable(Vec); - -impl RoaTable { - pub fn new(mut roas: Vec) -> Self { +impl BgpAnalysisReport { + pub fn new(mut roas: Vec) -> Self { roas.sort(); - RoaTable(roas) + BgpAnalysisReport(roas) } - pub fn entries(&self) -> &Vec { + pub fn entries(&self) -> &Vec { &self.0 } - fn matching_defs(&self, state: RoaTableEntryState) -> Vec<&RoaDefinition> { + pub fn matching_defs(&self, state: BgpAnalysisState) -> Vec<&RoaDefinition> { self.matching_entries(state) .into_iter() .map(|e| &e.definition) .collect() } - fn matching_entries(&self, state: RoaTableEntryState) -> Vec<&RoaTableEntry> { + pub fn matching_entries(&self, state: BgpAnalysisState) -> Vec<&BgpAnalysisEntry> { self.0.iter().filter(|e| e.state == state).collect() } } -impl fmt::Display for RoaTable { +impl fmt::Display for BgpAnalysisReport { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let entries = self.entries(); - let mut entry_map: HashMap> = HashMap::new(); + let mut entry_map: HashMap> = HashMap::new(); for entry in entries.into_iter() { let state = entry.state(); if !entry_map.contains_key(&state) { @@ -179,12 +45,12 @@ impl fmt::Display for RoaTable { entry_map.get_mut(&state).unwrap().push(entry); } - if entry_map.contains_key(&RoaTableEntryState::RoaNoAnnouncementInfo) { + if entry_map.contains_key(&BgpAnalysisState::RoaNoAnnouncementInfo) { write!(f, "no BGP announcements known") } else { - if let Some(valids) = entry_map.get(&RoaTableEntryState::RoaAuthorizing) { + if let Some(authorizing) = entry_map.get(&BgpAnalysisState::RoaAuthorizing) { writeln!(f, "Authorizations causing VALID announcements:")?; - for roa in valids { + for roa in authorizing { writeln!(f)?; writeln!(f, "\tDefinition: {}", roa.definition)?; writeln!(f)?; @@ -204,9 +70,9 @@ impl fmt::Display for RoaTable { writeln!(f)?; } - if let Some(invalids) = entry_map.get(&RoaTableEntryState::RoaDisallowing) { + if let Some(disallowing) = entry_map.get(&BgpAnalysisState::RoaDisallowing) { writeln!(f, "Authorizations causing INVALID announcements only:")?; - for roa in invalids { + for roa in disallowing { writeln!(f)?; writeln!(f, "\tDefinition: {}", roa.definition)?; writeln!(f)?; @@ -218,7 +84,7 @@ impl fmt::Display for RoaTable { writeln!(f)?; } - if let Some(stales) = entry_map.get(&RoaTableEntryState::RoaStale) { + if let Some(stales) = entry_map.get(&BgpAnalysisState::RoaStale) { writeln!( f, "Authorizations for which no announcements are found (possibly stale):" @@ -230,7 +96,16 @@ impl fmt::Display for RoaTable { writeln!(f)?; } - if let Some(invalid_asn) = entry_map.get(&RoaTableEntryState::AnnouncementInvalidAsn) { + if let Some(valids) = entry_map.get(&BgpAnalysisState::AnnouncementValid) { + writeln!(f, "Announcements which are valid:")?; + writeln!(f)?; + for ann in valids { + writeln!(f, "\tAnnouncement: {}", ann.definition)?; + } + writeln!(f)?; + } + + if let Some(invalid_asn) = entry_map.get(&BgpAnalysisState::AnnouncementInvalidAsn) { writeln!(f, "Announcements from an unauthorized ASN:")?; for ann in invalid_asn { writeln!(f)?; @@ -245,7 +120,7 @@ impl fmt::Display for RoaTable { } if let Some(invalid_length) = - entry_map.get(&RoaTableEntryState::AnnouncementInvalidLength) + entry_map.get(&BgpAnalysisState::AnnouncementInvalidLength) { writeln!(f, "Announcements from an authorized ASN, which are too specific (not allowed by max length):")?; for ann in invalid_length { @@ -260,8 +135,9 @@ impl fmt::Display for RoaTable { writeln!(f)?; } - if let Some(not_found) = entry_map.get(&RoaTableEntryState::AnnouncementNotFound) { + if let Some(not_found) = entry_map.get(&BgpAnalysisState::AnnouncementNotFound) { writeln!(f, "Announcements which are 'not found' (not covered by any of your authorizations):")?; + writeln!(f)?; for ann in not_found { writeln!(f, "\tAnnouncement: {}", ann.definition)?; } @@ -273,28 +149,209 @@ impl fmt::Display for RoaTable { } } -//------------ RoaSummary -------------------------------------------------- +//------------ BgpAnalysisEntry -------------------------------------------- #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] -pub struct RoaSummary(Vec); - -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] -pub struct RoaSummmaryEntry { +pub struct BgpAnalysisEntry { + #[serde(flatten)] definition: RoaDefinition, - state: RoaSummaryState, + state: BgpAnalysisState, + #[serde(skip_serializing_if = "Option::is_none")] + allowed_by: Option, + #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")] + disallowed_by: Vec, + #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")] + authorizes: Vec, + #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")] + disallows: Vec, } -impl fmt::Display for RoaSummmaryEntry { +impl BgpAnalysisEntry { + pub fn definition(&self) -> &RoaDefinition { + &self.definition + } + + pub fn state(&self) -> BgpAnalysisState { + self.state + } + + pub fn allowed_by(&self) -> Option<&RoaDefinition> { + self.allowed_by.as_ref() + } + + pub fn disallowed_by(&self) -> &Vec { + &self.disallowed_by + } + + pub fn authorizes(&self) -> &Vec { + &self.authorizes + } + + pub fn disallows(&self) -> &Vec { + &self.disallows + } + + pub fn roa_authorizing( + definition: RoaDefinition, + mut authorizes: Vec, + mut disallows: Vec, + ) -> Self { + authorizes.sort(); + disallows.sort(); + BgpAnalysisEntry { + definition, + state: BgpAnalysisState::RoaAuthorizing, + allowed_by: None, + disallowed_by: vec![], + authorizes, + disallows, + } + } + + pub fn roa_disallowing(definition: RoaDefinition, mut disallows: Vec) -> Self { + disallows.sort(); + BgpAnalysisEntry { + definition, + state: BgpAnalysisState::RoaDisallowing, + allowed_by: None, + disallowed_by: vec![], + authorizes: vec![], + disallows, + } + } + + pub fn roa_stale(definition: RoaDefinition) -> Self { + BgpAnalysisEntry { + definition, + state: BgpAnalysisState::RoaStale, + allowed_by: None, + disallowed_by: vec![], + authorizes: vec![], + disallows: vec![], + } + } + + pub fn roa_no_announcement_info(definition: RoaDefinition) -> Self { + BgpAnalysisEntry { + definition, + state: BgpAnalysisState::RoaNoAnnouncementInfo, + allowed_by: None, + disallowed_by: vec![], + authorizes: vec![], + disallows: vec![], + } + } + + pub fn announcement_valid(announcement: Announcement, allowed_by: RoaDefinition) -> Self { + BgpAnalysisEntry { + definition: RoaDefinition::from(announcement), + state: BgpAnalysisState::AnnouncementValid, + allowed_by: Some(allowed_by), + disallowed_by: vec![], + authorizes: vec![], + disallows: vec![], + } + } + + pub fn announcement_invalid_asn( + announcement: Announcement, + mut disallowed_by: Vec, + ) -> Self { + disallowed_by.sort(); + BgpAnalysisEntry { + definition: RoaDefinition::from(announcement), + state: BgpAnalysisState::AnnouncementInvalidAsn, + allowed_by: None, + disallowed_by, + authorizes: vec![], + disallows: vec![], + } + } + + pub fn announcement_invalid_length( + announcement: Announcement, + mut disallowed_by: Vec, + ) -> Self { + disallowed_by.sort(); + BgpAnalysisEntry { + definition: RoaDefinition::from(announcement), + state: BgpAnalysisState::AnnouncementInvalidLength, + allowed_by: None, + disallowed_by, + authorizes: vec![], + disallows: vec![], + } + } + + pub fn announcement_not_found(announcement: Announcement) -> Self { + BgpAnalysisEntry { + definition: RoaDefinition::from(announcement), + state: BgpAnalysisState::AnnouncementNotFound, + allowed_by: None, + disallowed_by: vec![], + authorizes: vec![], + disallows: vec![], + } + } +} + +impl Ord for BgpAnalysisEntry { + fn cmp(&self, other: &Self) -> Ordering { + let mut ordering = self.state.cmp(&other.state); + if ordering == Ordering::Equal { + ordering = self.definition.cmp(&other.definition); + } + ordering + } +} + +impl PartialOrd for BgpAnalysisEntry { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +//------------ BgpAnalysisState -------------------------------------------- + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialOrd, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum BgpAnalysisState { + RoaAuthorizing, + RoaDisallowing, + RoaStale, + AnnouncementValid, + AnnouncementInvalidLength, + AnnouncementInvalidAsn, + AnnouncementNotFound, + RoaNoAnnouncementInfo, +} + +//------------ AnnouncementReport ------------------------------------------ + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct AnnouncementReport(Vec); + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct AnnouncementReportEntry { + definition: RoaDefinition, + state: AnnouncementReportState, +} + +impl fmt::Display for AnnouncementReportEntry { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let state_str = match self.state { - RoaSummaryState::Valid => "announcement 'valid'", - RoaSummaryState::InvalidAsn => "announcement 'invalid': unauthorized asn", - RoaSummaryState::InvalidLength => "announcement 'invalid': more specific than allowed", - RoaSummaryState::NotFound => "announcement 'not found': not covered by your ROAs", - RoaSummaryState::Stale => { + AnnouncementReportState::Valid => "announcement 'valid'", + AnnouncementReportState::InvalidAsn => "announcement 'invalid': unauthorized asn", + AnnouncementReportState::InvalidLength => { + "announcement 'invalid': more specific than allowed" + } + AnnouncementReportState::NotFound => { + "announcement 'not found': not covered by your ROAs" + } + AnnouncementReportState::Stale => { "ROA does not cover any known announcement (stale or backup?)" } - RoaSummaryState::NoInfo => "ROA exists, but no bgp info currently available", + AnnouncementReportState::NoInfo => "ROA exists, but no bgp info currently available", }; write!(f, "{}\t{}", self.definition, state_str) } @@ -302,7 +359,7 @@ impl fmt::Display for RoaSummmaryEntry { #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] -pub enum RoaSummaryState { +pub enum AnnouncementReportState { Valid, InvalidAsn, InvalidLength, @@ -311,57 +368,53 @@ pub enum RoaSummaryState { NoInfo, } -impl From for RoaSummary { - fn from(table: RoaTable) -> Self { - let mut entries: Vec = vec![]; - for valid in table - .matching_entries(RoaTableEntryState::RoaAuthorizing) - .into_iter() - .flat_map(|e| &e.authorizes) - { - entries.push(RoaSummmaryEntry { - definition: valid.clone().into(), - state: RoaSummaryState::Valid, +impl From for AnnouncementReport { + fn from(table: BgpAnalysisReport) -> Self { + let mut entries: Vec = vec![]; + for def in table.matching_defs(BgpAnalysisState::AnnouncementValid) { + entries.push(AnnouncementReportEntry { + definition: def.clone(), + state: AnnouncementReportState::Valid, }) } - for def in table.matching_defs(RoaTableEntryState::AnnouncementInvalidAsn) { - entries.push(RoaSummmaryEntry { + for def in table.matching_defs(BgpAnalysisState::AnnouncementInvalidAsn) { + entries.push(AnnouncementReportEntry { definition: def.clone(), - state: RoaSummaryState::InvalidAsn, + state: AnnouncementReportState::InvalidAsn, }) } - for def in table.matching_defs(RoaTableEntryState::AnnouncementInvalidLength) { - entries.push(RoaSummmaryEntry { + for def in table.matching_defs(BgpAnalysisState::AnnouncementInvalidLength) { + entries.push(AnnouncementReportEntry { definition: def.clone(), - state: RoaSummaryState::InvalidLength, + state: AnnouncementReportState::InvalidLength, }) } - for def in table.matching_defs(RoaTableEntryState::AnnouncementNotFound) { - entries.push(RoaSummmaryEntry { + for def in table.matching_defs(BgpAnalysisState::AnnouncementNotFound) { + entries.push(AnnouncementReportEntry { definition: def.clone(), - state: RoaSummaryState::NotFound, + state: AnnouncementReportState::NotFound, }) } - for def in table.matching_defs(RoaTableEntryState::RoaStale) { - entries.push(RoaSummmaryEntry { + for def in table.matching_defs(BgpAnalysisState::RoaStale) { + entries.push(AnnouncementReportEntry { definition: def.clone(), - state: RoaSummaryState::Stale, + state: AnnouncementReportState::Stale, }) } - for def in table.matching_defs(RoaTableEntryState::RoaNoAnnouncementInfo) { - entries.push(RoaSummmaryEntry { + for def in table.matching_defs(BgpAnalysisState::RoaNoAnnouncementInfo) { + entries.push(AnnouncementReportEntry { definition: def.clone(), - state: RoaSummaryState::NoInfo, + state: AnnouncementReportState::NoInfo, }) } - RoaSummary(entries) + AnnouncementReport(entries) } } -impl fmt::Display for RoaSummary { +impl fmt::Display for AnnouncementReport { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for e in self.0.iter() { writeln!(f, "{}", e)?; @@ -374,22 +427,25 @@ impl fmt::Display for RoaSummary { #[cfg(test)] mod tests { - use crate::commons::bgp::{RoaSummary, RoaTable}; + use super::*; #[test] - fn print_roa_table() { - let json = include_str!("../../../test-resources/bgp/expected_roa_table.json"); - let table: RoaTable = serde_json::from_str(json).unwrap(); + fn print_bgp_report() { + let json = include_str!("../../../test-resources/bgp/expected_bgp_analyis_report.json"); + let report: BgpAnalysisReport = serde_json::from_str(json).unwrap(); - let expected_text = include_str!("../../../test-resources/bgp/expected_full_details.txt"); - assert_eq!(table.to_string(), expected_text); + let expected = include_str!("../../../test-resources/bgp/expected_bgp_analysis_report.txt"); + + print!("{}", report); + + assert_eq!(report.to_string(), expected); } #[test] fn print_roa_table_summary() { - let json = include_str!("../../../test-resources/bgp/expected_roa_table.json"); - let table: RoaTable = serde_json::from_str(json).unwrap(); - let summary: RoaSummary = table.into(); + let json = include_str!("../../../test-resources/bgp/expected_bgp_analyis_report.json"); + let report: BgpAnalysisReport = serde_json::from_str(json).unwrap(); + let summary: AnnouncementReport = report.into(); let expected_text = include_str!("../../../test-resources/bgp/expected_summary.txt"); assert_eq!(summary.to_string(), expected_text); diff --git a/src/daemon/http/server.rs b/src/daemon/http/server.rs index 281b0fa2..1bbf25e1 100644 --- a/src/daemon/http/server.rs +++ b/src/daemon/http/server.rs @@ -490,8 +490,8 @@ async fn api_ca_routes(req: Request, path: &mut RequestPath, ca: Handle) -> Rout Method::POST => ca_routes_update(req, ca).await, _ => render_unknown_method(), }, - Some("bgp") => match *req.method() { - Method::GET => ca_routes_bgp_analysis(req, ca).await, + Some("analysis") => match *req.method() { + Method::GET => ca_routes_analysis(req, path, ca).await, _ => render_unknown_method(), }, _ => render_unknown_method(), @@ -998,8 +998,18 @@ async fn ca_routes_show(req: Request, handle: Handle) -> RoutingResult { } /// Show the state of ROAs vs BGP for this CA -async fn ca_routes_bgp_analysis(req: Request, handle: Handle) -> RoutingResult { - render_json_res(req.state().read().await.ca_routes_bgp_analysis(&handle)) +async fn ca_routes_analysis(req: Request, path: &mut RequestPath, handle: Handle) -> RoutingResult { + match path.next() { + Some("full") => render_json_res(req.state().read().await.ca_routes_bgp_analysis(&handle)), + Some("announcements") => render_json_res( + req.state() + .read() + .await + .ca_routes_bgp_analysis_announcements(&handle), + ), + Some("roas") => unimplemented!(), + _ => render_unknown_method(), + } } //------------ Admin: Force republish ---------------------------------------- diff --git a/src/daemon/krillserver.rs b/src/daemon/krillserver.rs index 608fea0d..ecd7c2e8 100644 --- a/src/daemon/krillserver.rs +++ b/src/daemon/krillserver.rs @@ -18,7 +18,7 @@ use crate::commons::api::{ RepositoryContact, RepositoryUpdate, RoaDefinition, RoaDefinitionUpdates, ServerInfo, TaCertDetails, UpdateChildRequest, }; -use crate::commons::bgp::{BgpAnalyser, RoaTable}; +use crate::commons::bgp::{AnnouncementReport, BgpAnalyser, BgpAnalysisReport}; use crate::commons::error::Error; use crate::commons::eventsourcing::CommandKey; use crate::commons::remote::rfc8183; @@ -680,7 +680,7 @@ impl KrillServer { Ok(ca.roa_definitions()) } - pub fn ca_routes_bgp_analysis(&self, handle: &Handle) -> KrillResult { + pub fn ca_routes_bgp_analysis(&self, handle: &Handle) -> KrillResult { let ca = self.caserver.get_ca(handle)?; let definitions = ca.roa_definitions(); let resources = ca.all_resources(); @@ -688,6 +688,14 @@ impl KrillServer { .bgp_analyser .analyse(definitions.as_slice(), &resources)) } + + pub fn ca_routes_bgp_analysis_announcements( + &self, + handle: &Handle, + ) -> KrillResult { + let full = self.ca_routes_bgp_analysis(handle)?; + Ok(full.into()) + } } /// # Handle publication requests diff --git a/test-resources/bgp/expected_roa_table.json b/test-resources/bgp/expected_bgp_analyis_report.json similarity index 74% rename from test-resources/bgp/expected_roa_table.json rename to test-resources/bgp/expected_bgp_analyis_report.json index e2d9dcc2..b0164b69 100644 --- a/test-resources/bgp/expected_roa_table.json +++ b/test-resources/bgp/expected_bgp_analyis_report.json @@ -35,6 +35,26 @@ "prefix": "10.0.4.0/24", "state": "roa_stale" }, + { + "asn": 64496, + "prefix": "10.0.0.0/22", + "state": "announcement_valid", + "allowed_by": { + "asn": 64496, + "prefix": "10.0.0.0/22", + "max_length": 23 + } + }, + { + "asn": 64496, + "prefix": "10.0.2.0/23", + "state": "announcement_valid", + "allowed_by": { + "asn": 64496, + "prefix": "10.0.0.0/22", + "max_length": 23 + } + }, { "asn": 64496, "prefix": "10.0.0.0/24", diff --git a/test-resources/bgp/expected_full_details.txt b/test-resources/bgp/expected_bgp_analysis_report.txt similarity index 88% rename from test-resources/bgp/expected_full_details.txt rename to test-resources/bgp/expected_bgp_analysis_report.txt index f8f199b2..7ed6de50 100644 --- a/test-resources/bgp/expected_full_details.txt +++ b/test-resources/bgp/expected_bgp_analysis_report.txt @@ -15,6 +15,11 @@ Authorizations for which no announcements are found (possibly stale): Definition: 10.0.3.0/24 => 64497 Definition: 10.0.4.0/24 => 0 +Announcements which are valid: + + Announcement: 10.0.0.0/22 => 64496 + Announcement: 10.0.2.0/23 => 64496 + Announcements from an unauthorized ASN: Announcement: 10.0.0.0/22 => 64497 @@ -30,5 +35,6 @@ Announcements from an authorized ASN, which are too specific (not allowed by max 10.0.0.0/22-23 => 64496 Announcements which are 'not found' (not covered by any of your authorizations): + Announcement: 10.0.0.0/21 => 64497