Skip to content

Commit

Permalink
add bogons module to allow detecting bogon prefix and asns
Browse files Browse the repository at this point in the history
  • Loading branch information
digizeph committed Jun 24, 2024
1 parent 0acb54a commit 5d04f7f
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ regex = "1"
anyhow = "1.0"
chrono = { version = "0.4", features = ["serde"] }
ipnet-trie = "0.1.0"
ipnet = "2.8"
ipnet = { version = "2.9", features = ["serde"] }
tar = "0.4"

tracing = "0.1"
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,29 @@ let prefix = IpNet::from_str("1.1.1.0/24").unwrap();
assert_eq!(rpki.validate(&prefix, 13335), RpkiValidation::Valid);
```

### Bogon utilities

We provide a utility to check if an IP prefix or an ASN is a bogon.

#### Data sources

IANA special registries:
* IPv4: https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
* IPv6: https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
* ASN: https://www.iana.org/assignments/iana-as-numbers-special-registry/iana-as-numbers-special-registry.xhtml

#### Usage Examples

```rust
use bgpkit_commons::bogons::Bogons;

let bogons = Bogons::new().unwrap();
assert!(bogons.matches_str("10.0.0.0/9"));
assert!(bogons.matches_str("112"));
assert!(bogons.is_bogon_prefix(&"2001::/24".parse().unwrap()));
assert!(bogons.is_bogon_asn(65535));
```

### Feature Flags

- `rustls`: use rustls instead of native-tls for the underlying HTTPS requests
Expand Down
79 changes: 79 additions & 0 deletions src/bogons/asn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use crate::bogons::utils::{find_rfc_links, remove_footnotes, replace_commas_in_quotes};
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};

const IANA_ASN_SPECIAL_REGISTRY: &str = "https://www.iana.org/assignments/iana-as-numbers-special-registry/special-purpose-as-numbers.csv";

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BogonAsn {
pub asn_range: (u32, u32),
pub description: String,
pub rfc_urls: Vec<String>,
}

impl BogonAsn {
pub fn matches(&self, asn: u32) -> bool {
asn >= self.asn_range.0 && asn <= self.asn_range.1
}
}

fn convert_to_range(s: &str) -> Result<(u32, u32)> {
let parts: Vec<&str> = s.split('-').collect();
let start = parts[0].parse::<u32>()?;
let end = if parts.len() > 1 {
parts[1].parse::<u32>()?
} else {
start
};
Ok((start, end))
}

pub fn load_bogon_asns() -> Result<Vec<BogonAsn>> {
let mut bogons = Vec::new();
let mut prev_line: Option<String> = None;
for line in oneio::read_lines(IANA_ASN_SPECIAL_REGISTRY)? {
let mut text = line.ok().ok_or(anyhow!("error reading line"))?;
if text.trim() == "" || text.starts_with("AS") {
continue;
}
// remove triple quotes
text = text.replace("\"\"\"", "\"");
// remove footnote
text = remove_footnotes(text);
// replace commas in quotes
text = replace_commas_in_quotes(text.as_str());

let splits: Vec<&str> = text.split(',').map(|x| x.trim()).collect();
if splits.len() != 3 {
if prev_line.is_some() {
return Err(anyhow!("row missing fields: {}", text.as_str()));
}
prev_line = Some(text);
continue;
}
prev_line = None;

let asn_range = convert_to_range(splits[0].replace('\"', "").trim())?;
let description = splits[1].to_string();
let rfc_urls = find_rfc_links(splits[2]);

bogons.push(BogonAsn {
asn_range,
description,
rfc_urls,
});
}

Ok(bogons)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_load_bogon_asns() {
let bogons = load_bogon_asns().unwrap();
dbg!(bogons);
}
}
66 changes: 66 additions & 0 deletions src/bogons/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//! # Module: bogons
//!
//! This module provides functions to detect whether some given prefix or ASN is a bogon ASN.
//!
//! We obtain the bogon ASN and prefixes data from IANA's special registries:
//! * IPv4: https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
//! * IPv6: https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
//! * ASN: https://www.iana.org/assignments/iana-as-numbers-special-registry/iana-as-numbers-special-registry.xhtml
//!
//! The simplest way to check bogon is to provide a &str:
//! ```
//! let bogons = bgpkit_commons::bogons::Bogons::new().unwrap();
//! assert!(bogons.matches_str("10.0.0.0/9"));
//! assert!(bogons.matches_str("112"));
//! assert!(bogons.is_bogon_prefix(&"2001::/24".parse().unwrap()));
//! assert!(bogons.is_bogon_asn(65535));
//! ```
mod asn;
mod prefix;
mod utils;

use crate::bogons::asn::load_bogon_asns;
use crate::bogons::prefix::load_bogon_prefixes;
use anyhow::Result;
pub use asn::BogonAsn;
use ipnet::IpNet;
pub use prefix::BogonPrefix;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Bogons {
pub prefixes: Vec<BogonPrefix>,
pub asns: Vec<BogonAsn>,
}

impl Bogons {
pub fn new() -> Result<Self> {
Ok(Bogons {
prefixes: load_bogon_prefixes()?,
asns: load_bogon_asns()?,
})
}

/// Check if a given string matches a bogon prefix or ASN.
pub fn matches_str(&self, s: &str) -> bool {
match s.parse::<IpNet>() {
Ok(ip) => self.is_bogon_prefix(&ip),
Err(_) => match s.parse::<u32>() {
Ok(asn) => self.is_bogon_asn(asn),
Err(_) => false,
},
}
}

/// Check if a given IP prefix is a bogon prefix.
pub fn is_bogon_prefix(&self, prefix: &IpNet) -> bool {
self.prefixes
.iter()
.any(|bogon_prefix| bogon_prefix.matches(prefix))
}

/// Check if a given ASN is a bogon ASN.
pub fn is_bogon_asn(&self, asn: u32) -> bool {
self.asns.iter().any(|bogon_asn| bogon_asn.matches(asn))
}
}
110 changes: 110 additions & 0 deletions src/bogons/prefix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use crate::bogons::utils::{find_rfc_links, remove_footnotes, replace_commas_in_quotes};
use anyhow::{anyhow, Result};
use chrono::NaiveDate;
use ipnet::IpNet;
use serde::{Deserialize, Serialize};

const IANA_PREFIX_SPECIAL_REGISTRY: [&str; 2] = [
"https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry-1.csv",
"https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry-1.csv",
];

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BogonPrefix {
pub prefix: IpNet,
pub description: String,
pub rfc_urls: Vec<String>,
pub allocation_date: NaiveDate,
pub termination_date: Option<NaiveDate>,
pub source: bool,
pub destination: bool,
pub forwardable: bool,
pub global: bool,
pub reserved: bool,
}

impl BogonPrefix {
pub fn matches(&self, prefix: &IpNet) -> bool {
// if address family different, it is not a match
match (self.prefix, prefix) {
(IpNet::V4(_), IpNet::V6(_)) | (IpNet::V6(_), IpNet::V4(_)) => return false,
_ => {}
}
self.prefix.contains(prefix)
}
}

pub fn load_bogon_prefixes() -> Result<Vec<BogonPrefix>> {
let mut bogons = Vec::new();
for iana_link in IANA_PREFIX_SPECIAL_REGISTRY {
let mut prev_line: Option<String> = None;
for line in oneio::read_lines(iana_link)? {
let mut text = line.ok().ok_or(anyhow!("error reading line"))?;
if let Some(t) = &prev_line {
text = format!("{},{}", t, text);
}
if text.trim() == "" || text.starts_with("Address") {
continue;
}
// remove triple quotes
text = text.replace("\"\"\"", "\"");
// remove footnote
text = remove_footnotes(text);
// replace commas in quotes
text = replace_commas_in_quotes(text.as_str());

let splits: Vec<&str> = text.split(',').map(|x| x.trim()).collect();
if splits.len() != 10 {
if prev_line.is_some() {
return Err(anyhow!("row missing fields: {}", text.as_str()));
}
prev_line = Some(text);
continue;
}
prev_line = None;

let prefixes = splits[0]
.replace('\"', "")
.split(" ")
.map(|x| x.trim().parse::<IpNet>().unwrap())
.collect::<Vec<IpNet>>();
let description = splits[1].to_string();
let rfc_urls = find_rfc_links(splits[2]);
let allocation_date =
NaiveDate::parse_from_str(format!("{}-01", splits[3]).as_str(), "%Y-%m-%d")?;
let termination_date = match format!("{}-01", splits[3]).as_str() {
"" | "N/A" => None,
d => Some(NaiveDate::parse_from_str(d, "%Y-%m-%d")?),
};
let source = matches!(splits[5].to_lowercase().as_str(), "true");
let destination = matches!(splits[6].to_lowercase().as_str(), "true");
let forwardable = matches!(splits[7].to_lowercase().as_str(), "true");
let global = matches!(splits[8].to_lowercase().as_str(), "true");
let reserved = matches!(splits[9].to_lowercase().as_str(), "true");
bogons.extend(prefixes.into_iter().map(|prefix| BogonPrefix {
prefix,
description: description.clone(),
rfc_urls: rfc_urls.clone(),
allocation_date,
termination_date,
source,
destination,
forwardable,
global,
reserved,
}));
}
}
Ok(bogons)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_load_bogon_prefixes() {
let bogons = load_bogon_prefixes().unwrap();
dbg!(bogons);
}
}
29 changes: 29 additions & 0 deletions src/bogons/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use regex::Regex;

pub(crate) fn remove_footnotes(s: String) -> String {
let re = Regex::new(r"\[\d+\]").unwrap();
let result = re.replace_all(s.as_str(), "");
result.into_owned()
}

pub(crate) fn replace_commas_in_quotes(s: &str) -> String {
let re = Regex::new(r#""[^"]*""#).unwrap();
let result = re.replace_all(s, |caps: &regex::Captures| {
let matched = caps.get(0).unwrap().as_str();
matched.replace(",", "")
});
result.into_owned()
}

pub(crate) fn find_rfc_links(s: &str) -> Vec<String> {
let re = Regex::new(r"\[RFC(\d+)\]").unwrap();
let mut links = Vec::new();

for cap in re.captures_iter(s) {
let rfc_number = &cap[1];
let link = format!("https://datatracker.ietf.org/doc/html/rfc{}", rfc_number);
links.push(link);
}

links
}
24 changes: 24 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,29 @@
//! assert_eq!(rpki.validate(&prefix, 13335), RpkiValidation::Valid);
//! ```
//!
//! ## Bogon utilities
//!
//! We provide a utility to check if an IP prefix or an ASN is a bogon.
//!
//! ### Data sources
//!
//! IANA special registries:
//! * IPv4: https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
//! * IPv6: https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
//! * ASN: https://www.iana.org/assignments/iana-as-numbers-special-registry/iana-as-numbers-special-registry.xhtml
//!
//! ### Usage Examples
//!
//! ```
//! use bgpkit_commons::bogons::Bogons;
//!
//! let bogons = Bogons::new().unwrap();
//! assert!(bogons.matches_str("10.0.0.0/9"));
//! assert!(bogons.matches_str("112"));
//! assert!(bogons.is_bogon_prefix(&"2001::/24".parse().unwrap()));
//! assert!(bogons.is_bogon_asn(65535));
//! ```
//!
//! ## Feature Flags
//!
//! - `rustls`: use rustls instead of native-tls for the underlying HTTPS requests
Expand Down Expand Up @@ -219,6 +242,7 @@
)]

pub mod asnames;
pub mod bogons;
pub mod collectors;
pub mod countries;
pub mod rpki;

0 comments on commit 5d04f7f

Please sign in to comment.