-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add bogons module to allow detecting bogon prefix and asns
- Loading branch information
Showing
7 changed files
with
332 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: ®ex::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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters