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