1 use std::fmt; 2 3 use chrono::Duration; 4 use tokio::sync::RwLock; 5 6 use rpki::repository::x509::Time; 7 8 use crate::{ 9 commons::{ 10 api::{AsNumber, ResourceSet, RoaDefinition}, 11 bgp::{ 12 make_roa_tree, make_validated_announcement_tree, Announcement, AnnouncementValidity, Announcements, 13 BgpAnalysisEntry, BgpAnalysisReport, BgpAnalysisState, BgpAnalysisSuggestion, IpRange, RisDumpError, 14 RisDumpLoader, ValidatedAnnouncement, 15 }, 16 }, 17 constants::{test_announcements_enabled, BGP_RIS_REFRESH_MINUTES}, 18 }; 19 20 //------------ BgpAnalyser ------------------------------------------------- 21 22 /// This type helps analyse ROAs vs BGP and vice versa. 23 pub struct BgpAnalyser { 24 dump_loader: Option<RisDumpLoader>, 25 seen: RwLock<Announcements>, 26 } 27 28 impl BgpAnalyser { new(ris_enabled: bool, ris_v4_uri: &str, ris_v6_uri: &str) -> Self29 pub fn new(ris_enabled: bool, ris_v4_uri: &str, ris_v6_uri: &str) -> Self { 30 if test_announcements_enabled() { 31 Self::with_test_announcements() 32 } else { 33 let dump_loader = if ris_enabled { 34 Some(RisDumpLoader::new(ris_v4_uri, ris_v6_uri)) 35 } else { 36 None 37 }; 38 BgpAnalyser { 39 dump_loader, 40 seen: RwLock::new(Announcements::default()), 41 } 42 } 43 } 44 update(&self) -> Result<bool, BgpAnalyserError>45 pub async fn update(&self) -> Result<bool, BgpAnalyserError> { 46 if let Some(loader) = &self.dump_loader { 47 let mut seen = self.seen.write().await; 48 if let Some(last_time) = seen.last_checked() { 49 if (last_time + Duration::minutes(BGP_RIS_REFRESH_MINUTES)) > Time::now() { 50 trace!("Will not check BGP Ris Dumps until the refresh interval has passed"); 51 return Ok(false); // no need to update yet 52 } 53 } 54 let announcements = loader.download_updates().await?; 55 if seen.equivalent(&announcements) { 56 debug!("BGP Ris Dumps unchanged"); 57 seen.update_checked(); 58 Ok(false) 59 } else { 60 info!("Updated announcements ({}) based on BGP Ris Dumps", announcements.len()); 61 seen.update(announcements); 62 Ok(true) 63 } 64 } else { 65 Ok(false) 66 } 67 } 68 analyse( &self, roas: &[RoaDefinition], resources_held: &ResourceSet, limited_scope: Option<ResourceSet>, ) -> BgpAnalysisReport69 pub async fn analyse( 70 &self, 71 roas: &[RoaDefinition], 72 resources_held: &ResourceSet, 73 limited_scope: Option<ResourceSet>, 74 ) -> BgpAnalysisReport { 75 let seen = self.seen.read().await; 76 let mut entries = vec![]; 77 78 let roas: Vec<RoaDefinition> = match &limited_scope { 79 None => roas.to_vec(), 80 Some(limit) => roas 81 .iter() 82 .filter(|roa| limit.contains_roa_address(&roa.as_roa_ip_address())) 83 .cloned() 84 .collect(), 85 }; 86 87 let (roas, roas_not_held): (Vec<RoaDefinition>, _) = roas 88 .iter() 89 .partition(|roa| resources_held.contains_roa_address(&roa.as_roa_ip_address())); 90 91 for not_held in roas_not_held { 92 entries.push(BgpAnalysisEntry::roa_not_held(not_held)); 93 } 94 95 if seen.last_checked().is_none() { 96 // nothing to analyse, just push all ROAs as 'no announcement info' 97 for roa in roas { 98 entries.push(BgpAnalysisEntry::roa_no_announcement_info(roa)); 99 } 100 } else { 101 let scope = match &limited_scope { 102 Some(limit) => limit, 103 None => resources_held, 104 }; 105 106 let (v4_scope, v6_scope) = IpRange::for_resource_set(scope); 107 108 let mut scoped_announcements = vec![]; 109 110 for block in v4_scope.into_iter() { 111 scoped_announcements.append(&mut seen.contained_by(block)); 112 } 113 114 for block in v6_scope.into_iter() { 115 scoped_announcements.append(&mut seen.contained_by(block)); 116 } 117 118 let roa_tree = make_roa_tree(roas.as_ref()); 119 let validated: Vec<ValidatedAnnouncement> = scoped_announcements 120 .into_iter() 121 .map(|a| a.validate(&roa_tree)) 122 .collect(); 123 124 // Check all ROAs.. and report ROA state in relation to validated announcements 125 let validated_tree = make_validated_announcement_tree(validated.as_slice()); 126 for roa in roas { 127 let covered = validated_tree.matching_or_more_specific(&roa.prefix()); 128 129 let other_roas_covering_this_prefix: Vec<_> = roa_tree 130 .matching_or_less_specific(&roa.prefix()) 131 .into_iter() 132 .filter(|other| roa != **other) 133 .cloned() 134 .collect(); 135 136 let other_roas_including_this_definition: Vec<_> = other_roas_covering_this_prefix 137 .iter() 138 .filter(|other| { 139 other.asn() == roa.asn() 140 && other.prefix().addr_len() <= roa.prefix().addr_len() 141 && other.effective_max_length() >= roa.effective_max_length() 142 }) 143 .cloned() 144 .collect(); 145 146 let authorizes: Vec<Announcement> = covered 147 .iter() 148 .filter(|va| { 149 // VALID announcements under THIS ROA 150 // Already covered so it's under this ROA prefix 151 // ASN must match 152 // Prefix length must be allowed under this ROA (it could be allowed by another ROA and therefore valid) 153 va.validity() == AnnouncementValidity::Valid 154 && va.announcement().prefix().addr_len() <= roa.effective_max_length() 155 && va.announcement().asn() == &roa.asn() 156 }) 157 .map(|va| va.announcement()) 158 .collect(); 159 160 let disallows: Vec<Announcement> = covered 161 .iter() 162 .filter(|va| { 163 let validity = va.validity(); 164 validity == AnnouncementValidity::InvalidLength || validity == AnnouncementValidity::InvalidAsn 165 }) 166 .map(|va| va.announcement()) 167 .collect(); 168 169 let authorizes_excess = { 170 let max_length = roa.effective_max_length(); 171 let nr_of_specific_ann = authorizes 172 .iter() 173 .filter(|ann| ann.prefix().addr_len() == max_length) 174 .count() as u128; 175 176 nr_of_specific_ann > 0 && nr_of_specific_ann < roa.nr_of_specific_prefixes() 177 }; 178 179 if roa.asn() == AsNumber::zero() { 180 // see if this AS0 ROA is redundant, if it is mark it as such 181 if other_roas_covering_this_prefix.is_empty() { 182 // will disallow all covered announcements by definition (because AS0 announcements cannot exist) 183 let announcements = covered.iter().map(|va| va.announcement()).collect(); 184 entries.push(BgpAnalysisEntry::roa_as0(roa, announcements)); 185 } else { 186 entries.push(BgpAnalysisEntry::roa_as0_redundant( 187 roa, 188 other_roas_covering_this_prefix, 189 )); 190 } 191 } else if !other_roas_including_this_definition.is_empty() { 192 entries.push(BgpAnalysisEntry::roa_redundant( 193 roa, 194 authorizes, 195 disallows, 196 other_roas_including_this_definition, 197 )) 198 } else if authorizes.is_empty() && disallows.is_empty() { 199 entries.push(BgpAnalysisEntry::roa_unseen(roa)) 200 } else if authorizes_excess { 201 entries.push(BgpAnalysisEntry::roa_too_permissive(roa, authorizes, disallows)) 202 } else if authorizes.is_empty() { 203 entries.push(BgpAnalysisEntry::roa_disallowing(roa, disallows)) 204 } else { 205 entries.push(BgpAnalysisEntry::roa_seen(roa, authorizes, disallows)) 206 } 207 } 208 209 // Loop over all validated announcements and report 210 for v in validated.into_iter() { 211 let (announcement, validity, allowed_by, invalidating_roas) = v.unpack(); 212 match validity { 213 AnnouncementValidity::Valid => { 214 entries.push(BgpAnalysisEntry::announcement_valid( 215 announcement, 216 allowed_by.unwrap(), // always set for valid announcements 217 )) 218 } 219 AnnouncementValidity::Disallowed => { 220 entries.push(BgpAnalysisEntry::announcement_disallowed( 221 announcement, 222 invalidating_roas, 223 )); 224 } 225 AnnouncementValidity::InvalidLength => { 226 entries.push(BgpAnalysisEntry::announcement_invalid_length( 227 announcement, 228 invalidating_roas, 229 )); 230 } 231 AnnouncementValidity::InvalidAsn => { 232 entries.push(BgpAnalysisEntry::announcement_invalid_asn( 233 announcement, 234 invalidating_roas, 235 )); 236 } 237 AnnouncementValidity::NotFound => { 238 entries.push(BgpAnalysisEntry::announcement_not_found(announcement)); 239 } 240 } 241 } 242 } 243 BgpAnalysisReport::new(entries) 244 } 245 suggest( &self, roas: &[RoaDefinition], resources_held: &ResourceSet, limited_scope: Option<ResourceSet>, ) -> BgpAnalysisSuggestion246 pub async fn suggest( 247 &self, 248 roas: &[RoaDefinition], 249 resources_held: &ResourceSet, 250 limited_scope: Option<ResourceSet>, 251 ) -> BgpAnalysisSuggestion { 252 let mut suggestion = BgpAnalysisSuggestion::default(); 253 254 // perform analysis 255 let entries = self.analyse(roas, resources_held, limited_scope).await.into_entries(); 256 for entry in &entries { 257 match entry.state() { 258 BgpAnalysisState::RoaUnseen => suggestion.add_stale(*entry.definition()), 259 BgpAnalysisState::RoaTooPermissive => { 260 let replace_with = entry 261 .authorizes() 262 .iter() 263 .filter(|ann| { 264 !entries 265 .iter() 266 .any(|other| other != entry && other.authorizes().contains(*ann)) 267 }) 268 .map(|auth| RoaDefinition::from(*auth)) 269 .collect(); 270 271 suggestion.add_too_permissive(*entry.definition(), replace_with); 272 } 273 BgpAnalysisState::RoaSeen | BgpAnalysisState::RoaAs0 => suggestion.add_keep(*entry.definition()), 274 BgpAnalysisState::RoaDisallowing => suggestion.add_disallowing(*entry.definition()), 275 BgpAnalysisState::RoaRedundant => suggestion.add_redundant(*entry.definition()), 276 BgpAnalysisState::RoaNotHeld => suggestion.add_not_held(*entry.definition()), 277 BgpAnalysisState::RoaAs0Redundant => suggestion.add_as0_redundant(*entry.definition()), 278 BgpAnalysisState::AnnouncementValid => {} 279 BgpAnalysisState::AnnouncementNotFound => suggestion.add_not_found(entry.announcement()), 280 BgpAnalysisState::AnnouncementInvalidAsn => suggestion.add_invalid_asn(entry.announcement()), 281 BgpAnalysisState::AnnouncementInvalidLength => suggestion.add_invalid_length(entry.announcement()), 282 BgpAnalysisState::AnnouncementDisallowed => suggestion.add_keep_disallowing(entry.announcement()), 283 BgpAnalysisState::RoaNoAnnouncementInfo => suggestion.add_keep(*entry.definition()), 284 } 285 } 286 287 suggestion 288 } 289 test_announcements() -> Vec<Announcement>290 fn test_announcements() -> Vec<Announcement> { 291 use crate::test::announcement; 292 293 vec![ 294 announcement("10.0.0.0/22 => 64496"), 295 announcement("10.0.2.0/23 => 64496"), 296 announcement("10.0.0.0/24 => 64496"), 297 announcement("10.0.0.0/22 => 64497"), 298 announcement("10.0.0.0/21 => 64497"), 299 announcement("192.168.0.0/24 => 64497"), 300 announcement("192.168.0.0/24 => 64496"), 301 announcement("192.168.1.0/24 => 64497"), 302 announcement("2001:DB8::/32 => 64498"), 303 ] 304 } 305 with_test_announcements() -> Self306 fn with_test_announcements() -> Self { 307 let mut announcements = Announcements::default(); 308 announcements.update(Self::test_announcements()); 309 BgpAnalyser { 310 dump_loader: None, 311 seen: RwLock::new(announcements), 312 } 313 } 314 } 315 316 //------------ Error -------------------------------------------------------- 317 318 #[derive(Debug)] 319 pub enum BgpAnalyserError { 320 RisDump(RisDumpError), 321 } 322 323 impl fmt::Display for BgpAnalyserError { fmt(&self, f: &mut fmt::Formatter) -> fmt::Result324 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 325 match self { 326 BgpAnalyserError::RisDump(e) => write!(f, "BGP RIS update error: {}", e), 327 } 328 } 329 } 330 331 impl From<RisDumpError> for BgpAnalyserError { from(e: RisDumpError) -> Self332 fn from(e: RisDumpError) -> Self { 333 BgpAnalyserError::RisDump(e) 334 } 335 } 336 337 //------------ Tests -------------------------------------------------------- 338 339 #[cfg(test)] 340 mod tests { 341 342 use crate::commons::api::RoaDefinitionUpdates; 343 use crate::commons::bgp::BgpAnalysisState; 344 use crate::test::*; 345 346 use super::*; 347 348 #[tokio::test] 349 #[ignore] download_ris_dumps()350 async fn download_ris_dumps() { 351 let bgp_ris_dump_v4_uri = "http://www.ris.ripe.net/dumps/riswhoisdump.IPv4.gz"; 352 let bgp_ris_dump_v6_uri = "http://www.ris.ripe.net/dumps/riswhoisdump.IPv6.gz"; 353 354 let analyser = BgpAnalyser::new(true, bgp_ris_dump_v4_uri, bgp_ris_dump_v6_uri); 355 356 assert!(analyser.seen.read().await.is_empty()); 357 assert!(analyser.seen.read().await.last_checked().is_none()); 358 analyser.update().await.unwrap(); 359 assert!(!analyser.seen.read().await.is_empty()); 360 assert!(analyser.seen.read().await.last_checked().is_some()); 361 } 362 363 #[tokio::test] analyse_bgp()364 async fn analyse_bgp() { 365 let roa_too_permissive = definition("10.0.0.0/22-23 => 64496"); 366 let roa_as0 = definition("10.0.4.0/24 => 0"); 367 let roa_unseen_completely = definition("10.0.3.0/24 => 64497"); 368 369 let roa_not_held = definition("10.1.0.0/24 => 64497"); 370 371 let roa_authorizing_single = definition("192.168.1.0/24 => 64497"); 372 let roa_unseen_redundant = definition("192.168.1.0/24 => 64498"); 373 let roa_as0_redundant = definition("192.168.1.0/24 => 0"); 374 375 let resources_held = ResourceSet::from_strs("", "10.0.0.0/16, 192.168.0.0/16", "").unwrap(); 376 let limit = None; 377 378 let analyser = BgpAnalyser::with_test_announcements(); 379 380 let report = analyser 381 .analyse( 382 &[ 383 roa_too_permissive, 384 roa_as0, 385 roa_unseen_completely, 386 roa_not_held, 387 roa_authorizing_single, 388 roa_unseen_redundant, 389 roa_as0_redundant, 390 ], 391 &resources_held, 392 limit, 393 ) 394 .await; 395 396 let expected: BgpAnalysisReport = 397 serde_json::from_str(include_str!("../../../test-resources/bgp/expected_full_report.json")).unwrap(); 398 399 assert_eq!(report, expected); 400 } 401 402 #[tokio::test] analyse_bgp_disallowed_announcements()403 async fn analyse_bgp_disallowed_announcements() { 404 let roa = definition("10.0.0.0/22 => 0"); 405 let analyser = BgpAnalyser::with_test_announcements(); 406 407 let resources_held = ResourceSet::from_strs("", "10.0.0.0/8, 192.168.0.0/16", "").unwrap(); 408 let report = analyser.analyse(&[roa], &resources_held, None).await; 409 410 assert!(!report.contains_invalids()); 411 412 let mut disallowed = report.matching_defs(BgpAnalysisState::AnnouncementDisallowed); 413 disallowed.sort(); 414 415 let disallowed_1 = definition("10.0.0.0/22 => 64496"); 416 let disallowed_2 = definition("10.0.0.0/22 => 64497"); 417 let disallowed_3 = definition("10.0.0.0/24 => 64496"); 418 let disallowed_4 = definition("10.0.2.0/23 => 64496"); 419 let mut expected = vec![&disallowed_1, &disallowed_2, &disallowed_3, &disallowed_4]; 420 expected.sort(); 421 422 assert_eq!(disallowed, expected); 423 424 let suggestion = analyser.suggest(&[roa], &resources_held, None).await; 425 let updates = RoaDefinitionUpdates::from(suggestion); 426 427 let added = updates.added(); 428 for def in disallowed { 429 assert!(!added.contains(def)) 430 } 431 } 432 433 #[tokio::test] analyse_bgp_no_announcements()434 async fn analyse_bgp_no_announcements() { 435 let roa1 = definition("10.0.0.0/23-24 => 64496"); 436 let roa2 = definition("10.0.3.0/24 => 64497"); 437 let roa3 = definition("10.0.4.0/24 => 0"); 438 439 let resources_held = ResourceSet::from_strs("", "10.0.0.0/16", "").unwrap(); 440 441 let analyser = BgpAnalyser::new(false, "", ""); 442 let table = analyser.analyse(&[roa1, roa2, roa3], &resources_held, None).await; 443 let table_entries = table.entries(); 444 assert_eq!(3, table_entries.len()); 445 446 let roas_no_info: Vec<&RoaDefinition> = table_entries 447 .iter() 448 .filter(|e| e.state() == BgpAnalysisState::RoaNoAnnouncementInfo) 449 .map(|e| e.definition()) 450 .collect(); 451 452 assert_eq!(roas_no_info.as_slice(), &[&roa1, &roa2, &roa3]); 453 } 454 455 #[tokio::test] make_bgp_analysis_suggestion()456 async fn make_bgp_analysis_suggestion() { 457 let roa_too_permissive = definition("10.0.0.0/22-23 => 64496"); 458 let roa_redundant = definition("10.0.0.0/23 => 64496"); 459 let roa_as0 = definition("10.0.4.0/24 => 0"); 460 let roa_unseen_completely = definition("10.0.3.0/24 => 64497"); 461 let roa_authorizing_single = definition("192.168.1.0/24 => 64497"); 462 let roa_unseen_redundant = definition("192.168.1.0/24 => 64498"); 463 let roa_as0_redundant = definition("192.168.1.0/24 => 0"); 464 465 let analyser = BgpAnalyser::with_test_announcements(); 466 467 let resources_held = ResourceSet::from_strs("", "10.0.0.0/8, 192.168.0.0/16", "").unwrap(); 468 let limit = Some(ResourceSet::from_strs("", "10.0.0.0/22", "").unwrap()); 469 let suggestion_resource_subset = analyser 470 .suggest( 471 &[ 472 roa_too_permissive, 473 roa_redundant, 474 roa_as0, 475 roa_unseen_completely, 476 roa_authorizing_single, 477 roa_unseen_redundant, 478 roa_as0_redundant, 479 ], 480 &resources_held, 481 limit, 482 ) 483 .await; 484 485 let expected: BgpAnalysisSuggestion = serde_json::from_str(include_str!( 486 "../../../test-resources/bgp/expected_suggestion_some_roas.json" 487 )) 488 .unwrap(); 489 assert_eq!(suggestion_resource_subset, expected); 490 491 let suggestion_all_roas_in_scope = analyser 492 .suggest( 493 &[ 494 roa_too_permissive, 495 roa_redundant, 496 roa_as0, 497 roa_unseen_completely, 498 roa_authorizing_single, 499 roa_unseen_redundant, 500 roa_as0_redundant, 501 ], 502 &resources_held, 503 None, 504 ) 505 .await; 506 507 let expected: BgpAnalysisSuggestion = serde_json::from_str(include_str!( 508 "../../../test-resources/bgp/expected_suggestion_all_roas.json" 509 )) 510 .unwrap(); 511 512 assert_eq!(suggestion_all_roas_in_scope, expected); 513 } 514 } 515