routinator/
validity.rs

1//! Checking for validity of route announcements.
2
3use std::{fmt, io};
4use std::str::FromStr;
5use chrono::{DateTime, Utc};
6use rpki::resources::{Asn, Prefix};
7use rpki::rtr::payload::RouteOrigin;
8use serde::Deserialize;
9use crate::payload::{PayloadInfo, PayloadSnapshot};
10use crate::utils::date::format_iso_date;
11
12
13//------------ RouteValidityList ---------------------------------------------
14
15/// Information about the RPKI validity of route announcements.
16#[derive(Clone, Debug)]
17pub struct RouteValidityList<'a> {
18    routes: Vec<RouteValidity<'a>>,
19    created: DateTime<Utc>,
20}
21
22impl<'a> RouteValidityList<'a> {
23    /// Creates a value from requests and a snapshot.
24    fn from_requests(
25        requests: &RequestList, snapshot: &'a PayloadSnapshot
26    ) -> Self {
27        RouteValidityList {
28            routes: requests.routes.iter().map(|route| {
29                RouteValidity::new(route.prefix, route.asn, snapshot)
30            }).collect(),
31            created: snapshot.created(),
32        }
33    }
34
35    pub fn write_plain<W: io::Write>(
36        &self,
37        target: &mut W
38    ) -> Result<(), io::Error> {
39        for route in &self.routes {
40            route.write_plain(target)?;
41        }
42        Ok(())
43    }
44
45    pub fn write_json<W: io::Write>(
46        &self,
47        target: &mut W
48    ) -> Result<(), io::Error> {
49        writeln!(target, "{{\n  \"validated_routes\": [")?;
50        let mut first = true;
51        for route in &self.routes {
52            if first {
53                first = false;
54            }
55            else {
56                writeln!(target, ",")?;
57            }
58            write!(target, "    ")?;
59            route.write_single_json("    ", target)?;
60        }
61        writeln!(target,
62            "\n  ],\
63            \n  \"generatedTime\": \"{}\"\
64            \n}}",
65            format_iso_date(self.created),
66        )
67    }
68
69    pub fn iter_state(
70        &self
71    ) -> impl Iterator<Item = (Prefix, Asn, RouteState)> + '_ {
72        self.routes.iter().map(|route| {
73            (route.prefix, route.asn, route.state())
74        })
75    }
76}
77
78
79//------------ RouteValidity -------------------------------------------------
80
81/// Information about the RPKI validity of a single route announcement.
82#[derive(Clone, Debug)]
83pub struct RouteValidity<'a> {
84    /// The address prefix of the route announcement.
85    prefix: Prefix,
86
87    /// The origin AS number of the route announcement.
88    asn: Asn,
89
90    /// Indexes of the matched VRPs in `origins`.
91    matched: Vec<(RouteOrigin, &'a PayloadInfo)>,
92
93    /// Indexes of covering VRPs that don’t match because of the ´asn`.
94    bad_asn: Vec<(RouteOrigin, &'a PayloadInfo)>,
95
96    /// Indexes of covering VRPs that don’t match because of the prefix length.
97    bad_len: Vec<(RouteOrigin, &'a PayloadInfo)>,
98}
99
100impl<'a> RouteValidity<'a> {
101    pub fn new(
102        prefix: Prefix,
103        asn: Asn,
104        snapshot: &'a PayloadSnapshot
105    ) -> Self {
106        let mut matched = Vec::new();
107        let mut bad_asn = Vec::new();
108        let mut bad_len = Vec::new();
109        for item in snapshot.origins() {
110            if item.0.prefix.prefix().covers(prefix) {
111                if prefix.len() > item.0.prefix.resolved_max_len() {
112                    bad_len.push(item);
113                }
114                else if item.0.asn != asn {
115                    bad_asn.push(item);
116                }
117                else {
118                    matched.push(item)
119                }
120            }
121        }
122        RouteValidity { prefix, asn, matched, bad_asn, bad_len }
123    }
124
125    pub fn prefix(&self) -> Prefix {
126        self.prefix
127    }
128
129    pub fn asn(&self) -> Asn {
130        self.asn
131    }
132
133    pub fn state(&self) -> RouteState {
134        if self.matched.is_empty() {
135            if self.bad_asn.is_empty() && self.bad_len.is_empty() {
136                RouteState::NotFound
137            }
138            else {
139                RouteState::Invalid
140            }
141        }
142        else {
143            RouteState::Valid
144        }
145    }
146
147    pub fn reason(&self) -> Option<&'static str> {
148        if self.matched.is_empty() {
149            if !self.bad_asn.is_empty() {
150                Some("as")
151            }
152            else if !self.bad_len.is_empty() {
153                Some("length")
154            }
155            else {
156                None
157            }
158        }
159        else {
160            None
161        }
162    }
163
164    pub fn description(&self) -> &'static str {
165        if self.matched.is_empty() {
166            if !self.bad_asn.is_empty() {
167                DESCRIPTION_BAD_ASN
168            }
169            else if !self.bad_len.is_empty() {
170                DESCRIPTION_BAD_LEN
171            }
172            else {
173                DESCRIPTION_NOT_FOUND
174            }
175        }
176        else {
177            DESCRIPTION_VALID
178        }
179    }
180
181    pub fn matched(&self) -> &[(RouteOrigin, &'a PayloadInfo)] {
182        &self.matched
183    }
184
185    pub fn bad_asn(&self) -> &[(RouteOrigin, &'a PayloadInfo)] {
186        &self.bad_asn
187    }
188
189    pub fn bad_len(&self) -> &[(RouteOrigin, &'a PayloadInfo)] {
190        &self.bad_len
191    }
192
193    pub fn write_plain<W: io::Write>(
194        &self,
195        target: &mut W
196    ) -> Result<(), io::Error> {
197        writeln!(target, "{} => {}: {}", self.prefix, self.asn, self.state())
198    }
199
200    pub fn into_json(self, current: &PayloadSnapshot) -> Vec<u8> {
201        let mut res = Vec::new();
202        self.write_json(current, &mut res).unwrap();
203        res
204    }
205
206    pub fn write_json<W: io::Write>(
207        &self,
208        current: &PayloadSnapshot,
209        target: &mut W
210    ) -> Result<(), io::Error> {
211        write!(target, "{{\n  \"validated_route\": ")?;
212        self.write_single_json("  ", target)?;
213        writeln!(target,
214            ",\n  \"generatedTime\": \"{}\"\
215            \n}}",
216            format_iso_date(current.created()),
217        )
218    }
219
220    fn write_single_json<W: io::Write>(
221        &self,
222        indent: &str,
223        target: &mut W
224    ) -> Result<(), io::Error> {
225        writeln!(target, "{{\n\
226            {indent}  \"route\": {{\n\
227            {indent}    \"origin_asn\": \"{}\",\n\
228            {indent}    \"prefix\": \"{}\"\n\
229            {indent}  }},\n\
230            {indent}  \"validity\": {{\n\
231            {indent}    \"state\": \"{}\",",  
232            self.asn,
233            self.prefix,
234            self.state(),
235            indent = indent,
236        )?;
237        if let Some(reason) = self.reason() {
238            writeln!(target, "{indent}    \"reason\": \"{reason}\",")?;
239        }
240        writeln!(
241            target,
242            "{indent}    \"description\": \"{}\",\n\
243             {indent}    \"VRPs\": {{",
244            self.description(), indent = indent
245        )?;
246
247        Self::write_vrps_json(
248            indent, "matched", &self.matched, target
249        )?;
250        writeln!(target, ",")?;
251        Self::write_vrps_json(
252            indent, "unmatched_as", &self.bad_asn, target
253        )?;
254        writeln!(target, ",")?;
255        Self::write_vrps_json(
256            indent, "unmatched_length", &self.bad_len, target
257        )?;
258
259        write!(
260            target, "\n\
261            {indent}    }}\n\
262            {indent}  }}\n\
263            {indent}}}"
264        )
265    }
266
267    fn write_vrps_json<W: io::Write>(
268        indent: &str,
269        category: &str,
270        vrps: &[(RouteOrigin, &'a PayloadInfo)],
271        target: &mut W
272    ) -> Result<(), io::Error> {
273        write!(target, "{indent}      \"{category}\": [")?;
274        let mut first = true;
275        for item in vrps.iter() {
276            if first {
277                first = false;
278            }
279            else {
280                write!(target, ",")?;
281            }
282
283            write!(
284                target,
285                "\n\
286                {indent}        {{\n\
287                {indent}          \"asn\": \"{}\",\n\
288                {indent}          \"prefix\": \"{}\",\n\
289                {indent}          \"max_length\": \"{}\"\n\
290                {indent}        }}",
291                item.0.asn,
292                item.0.prefix.prefix(),
293                item.0.prefix.resolved_max_len(),
294                indent = indent
295            )?
296        }
297        write!(target, "\n{indent}      ]")
298    }
299
300}
301
302
303//------------ RouteState ----------------------------------------------------
304
305/// The RPKI state of a route announcement.
306///
307/// These states are defined in [RFC 6811] and determine whether a route
308/// announcement has passed by RPKI route origin validation.
309///
310/// The states are determined based on two terms:
311///
312/// * A VRP is said to _cover_ an announcement if its prefix covers the
313///   announcement, that is the VRP’s prefixes length is less or equal and
314///   the bits of its network prefix match the respective bits of the
315///   announcment’s prefix.
316/// * A VRP is said to _match_ an announcement if it covers the announcment
317///   and in addition the announcement’s origin AS number is equal to the
318///   VRP’s AS number and the announcement’s prefix length is less or equal
319///   to the VRP’s maximum length (which is considered equal to the prefix
320///   length if absent).
321///
322/// With these definitions in mind, there are three states described by the
323/// three variants of this enum.
324///
325/// [RFC 6811]: https://tools.ietf.org/html/rfc6811
326#[derive(Clone, Copy, Debug)]
327pub enum RouteState {
328    /// RPKI Valid.
329    ///
330    /// At least one VRP matches the annoncement.
331    Valid,
332
333    /// RPKI Invalid.
334    ///
335    /// At least one VRP covers the announcement but no VRP matches it.
336    Invalid,
337
338    /// RPKI Not Found.
339    ///
340    /// No VRP covers the announcement.
341    NotFound
342}
343
344impl fmt::Display for RouteState {
345    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
346        f.write_str(match *self {
347            RouteState::Valid => "valid",
348            RouteState::Invalid => "invalid",
349            RouteState::NotFound => "not-found",
350        })
351    }
352}
353
354
355//------------ RequestList ---------------------------------------------------
356
357/// A list of requests for route validity checks.
358///
359/// This type is intended to be used for deserialization of such a list from a
360/// file.
361#[derive(Clone, Debug, Default, Deserialize)]
362pub struct RequestList {
363    /// All the requests.
364    routes: Vec<Request>,
365}
366
367impl RequestList {
368    /// Loads the request list from a plain text reader.
369    pub fn from_plain_reader<R: io::BufRead>(
370        reader: R
371    ) -> Result<Self, io::Error>
372    {
373        let mut res = Self::default();
374
375        for (line_no, line) in reader.lines().enumerate() {
376            let line = line?;
377            let mut tokens = line.split_whitespace();
378
379            // PREFIX => ASN [# anything ]
380
381            let prefix = match tokens.next() {
382                Some(prefix) => {
383                    match Prefix::from_str(prefix) {
384                        Ok(prefix) => prefix,
385                        Err(_) => {
386                            return Err(io::Error::other(
387                                format!(
388                                    "line {}: expecting prefix, got '{}'",
389                                    line_no + 1, prefix
390                                )
391                            ))
392                        }
393                    }
394                }
395                None => continue
396            };
397
398            match tokens.next() {
399                Some("=>") => { }
400                Some(token) => {
401                    return Err(io::Error::other(
402                        format!(
403                            "line {}: expecting '=>', got '{}'",
404                            line_no + 1, token
405                        )
406                    ))
407                }
408                None => {
409                    return Err(io::Error::other(
410                        format!(
411                            "line {}: expecting '=>', got end of line",
412                            line_no + 1
413                        )
414                    ))
415                }
416            }
417
418            let asn = match tokens.next() {
419                Some(asn) => {
420                    match Asn::from_str(asn) {
421                        Ok(asn) => asn,
422                        Err(_) => {
423                            return Err(io::Error::other(
424                                format!(
425                                    "line {}: expecting AS number, got '{}'",
426                                    line_no + 1, asn
427                                )
428                            ))
429                        }
430                    }
431                }
432                None => {
433                    return Err(io::Error::other(
434                        format!(
435                            "line {}: expecting AS number, got end of line",
436                            line_no + 1
437                        )
438                    ))
439                }
440            };
441
442            match tokens.next() {
443                Some("#") | None => { }
444                Some(token) => {
445                    return Err(io::Error::other(
446                        format!(
447                            "line {}: expecting '#'  or end of line, got '{}'",
448                            line_no + 1, token
449                        )
450                    ))
451                }
452            }
453
454            res.routes.push(Request { prefix, asn });
455        }
456
457        Ok(res)
458    }
459
460    /// Loads the request list from a json-formatted reader.
461    pub fn from_json_reader<R: io::Read>(
462        reader: &mut R
463    ) -> Result<Self, serde_json::Error> {
464        serde_json::from_reader(reader)
465    }
466
467    /// Creates a request list with a single entry.
468    pub fn single(prefix: Prefix, asn: Asn) -> Self {
469        RequestList {
470            routes: vec![Request { prefix, asn }]
471        }
472    }
473
474    /// Checks the validity of all routes and returns a vec with results.
475    pub fn validity<'a>(
476        &self,
477        snapshot: &'a PayloadSnapshot
478    ) -> RouteValidityList<'a> {
479        RouteValidityList::from_requests(self, snapshot)
480    }
481}
482
483
484//------------ Request -------------------------------------------------------
485
486/// A request for a route validity check.
487#[derive(Clone, Debug, Deserialize)]
488struct Request {
489    /// The address prefix of the route announcement.
490    prefix: Prefix,
491
492    /// The origin AS number of the route announcement.
493    #[serde(deserialize_with = "Asn::deserialize_from_any")]
494    asn: Asn,
495}
496
497
498//------------ Constants -----------------------------------------------------
499
500// Description texts as provided by the RIPE NCC Validator.
501//
502const DESCRIPTION_VALID: &str = "At least one VRP Matches the Route Prefix";
503const DESCRIPTION_BAD_ASN: &str = "At least one VRP Covers the Route Prefix, \
504                                   but no VRP ASN matches the route origin \
505                                   ASN";
506const DESCRIPTION_BAD_LEN: &str = "At least one VRP Covers the Route Prefix, \
507                                   but the Route Prefix length is greater \
508                                   than the maximum length allowed by VRP(s) \
509                                   matching this route origin ASN";
510const DESCRIPTION_NOT_FOUND: &str = "No VRP Covers the Route Prefix";
511
512
513//============ Tests =========================================================
514
515#[cfg(test)]
516mod test {
517    use super::*;
518
519    #[test]
520    fn request_list_from_json_reader() {
521        let _ = RequestList::from_json_reader(
522            &mut include_bytes!("../test/validate/beacons.json").as_ref()
523        );
524    }
525}
526