Make specific end-point for announcement centric ROA vs BGP analysis. (closes: #241)

This commit is contained in:
Tim Bruijnzeels
2020-05-29 17:06:17 +02:00
parent 3167775f1f
commit f4ff7e3d36
11 changed files with 406 additions and 277 deletions
+8 -10
View File
@@ -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) => {
+43 -18
View File
@@ -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<Options, Error> {
fn parse_matches_cas_routes_bgp_full(matches: &ArgMatches) -> Result<Options, Error> {
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<Options, Error> {
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<Options, Error> {
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<Options, Error> {
@@ -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)]
+7 -9
View File
@@ -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<RoaDefinition>),
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<RoaDefinition> {
}
}
impl Report for RoaTable {
impl Report for BgpAnalysisReport {
fn text(&self) -> Result<String, ReportError> {
Ok(self.to_string())
}
}
impl Report for RoaSummary {
impl Report for AnnouncementReport {
fn text(&self) -> Result<String, ReportError> {
Ok(self.to_string())
}
+26 -18
View File
@@ -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<RisDumpLoader>,
@@ -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<Announcement> = 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<RisDumpError> 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();
+10 -10
View File
@@ -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<RoaDefinition>,
invalidating: Vec<RoaDefinition>,
authorizing: Option<RoaDefinition>,
disallowing: Vec<RoaDefinition>,
}
impl ValidatedAnnouncement {
@@ -251,8 +251,8 @@ impl ValidatedAnnouncement {
(
self.announcement,
self.validity,
self.validating,
self.invalidating,
self.authorizing,
self.disallowing,
)
}
}
+2 -2
View File
@@ -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::*;
@@ -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<RoaDefinition>,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
authorizes: Vec<Announcement>,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
disallows: Vec<Announcement>,
}
pub struct BgpAnalysisReport(Vec<BgpAnalysisEntry>);
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<Announcement>,
mut disallows: Vec<Announcement>,
) -> Self {
authorizes.sort();
disallows.sort();
RoaTableEntry {
definition,
state: RoaTableEntryState::RoaAuthorizing,
disallowed_by: vec![],
authorizes,
disallows,
}
}
pub fn roa_disallowing(definition: RoaDefinition, mut disallows: Vec<Announcement>) -> 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<RoaDefinition>,
) -> 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<RoaDefinition>,
) -> 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<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct RoaTable(Vec<RoaTableEntry>);
impl RoaTable {
pub fn new(mut roas: Vec<RoaTableEntry>) -> Self {
impl BgpAnalysisReport {
pub fn new(mut roas: Vec<BgpAnalysisEntry>) -> Self {
roas.sort();
RoaTable(roas)
BgpAnalysisReport(roas)
}
pub fn entries(&self) -> &Vec<RoaTableEntry> {
pub fn entries(&self) -> &Vec<BgpAnalysisEntry> {
&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<RoaTableEntryState, Vec<&RoaTableEntry>> = HashMap::new();
let mut entry_map: HashMap<BgpAnalysisState, Vec<&BgpAnalysisEntry>> = 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<RoaSummmaryEntry>);
#[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<RoaDefinition>,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
disallowed_by: Vec<RoaDefinition>,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
authorizes: Vec<Announcement>,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
disallows: Vec<Announcement>,
}
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<RoaDefinition> {
&self.disallowed_by
}
pub fn authorizes(&self) -> &Vec<Announcement> {
&self.authorizes
}
pub fn disallows(&self) -> &Vec<Announcement> {
&self.disallows
}
pub fn roa_authorizing(
definition: RoaDefinition,
mut authorizes: Vec<Announcement>,
mut disallows: Vec<Announcement>,
) -> 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<Announcement>) -> 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<RoaDefinition>,
) -> 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<RoaDefinition>,
) -> 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<Ordering> {
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<AnnouncementReportEntry>);
#[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<RoaTable> for RoaSummary {
fn from(table: RoaTable) -> Self {
let mut entries: Vec<RoaSummmaryEntry> = 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<BgpAnalysisReport> for AnnouncementReport {
fn from(table: BgpAnalysisReport) -> Self {
let mut entries: Vec<AnnouncementReportEntry> = 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);
+14 -4
View File
@@ -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 ----------------------------------------
+10 -2
View File
@@ -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<RoaTable> {
pub fn ca_routes_bgp_analysis(&self, handle: &Handle) -> KrillResult<BgpAnalysisReport> {
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<AnnouncementReport> {
let full = self.ca_routes_bgp_analysis(handle)?;
Ok(full.into())
}
}
/// # Handle publication requests
@@ -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",
@@ -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