Skip to content

Commit

Permalink
Merge pull request #50 from bgpkit/radar
Browse files Browse the repository at this point in the history
add Cloudflare Radar API queries support
  • Loading branch information
digizeph authored Jul 24, 2023
2 parents 8f91509 + 7dffa8e commit 87b2d1a
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 2 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ Cargo.lock
**/*.rs.bk

.idea
*.sqlite3
*.sqlite3

.env*
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ ureq = {version="2.6.2", features=["json"]}
regex = "1.6.0"
oneio = "0.11.0"
rpki = {version= "0.16.1", features = ["repository"]}
radar-rs = "0.0.2"

# progress bar
indicatif = "0.17.0"
Expand Down
2 changes: 2 additions & 0 deletions src/datasets/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod as2org;
mod country;
mod radar;
mod rpki;

pub use crate::datasets::as2org::*;
pub use crate::datasets::country::*;
pub use crate::datasets::radar::*;
pub use crate::datasets::rpki::*;
35 changes: 35 additions & 0 deletions src/datasets/radar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! Cloudflare Radar data access.
//!
//! Note: using this module requires setting CF_API_TOKEN in environment variables

use radar_rs::{PrefixOriginsResult, RadarClient, RadarError, RoutingStatsResult};

pub struct CfRadar {
client: RadarClient,
}

impl CfRadar {
pub fn new() -> Result<Self, RadarError> {
Ok(Self {
client: RadarClient::new()?,
})
}

pub fn get_bgp_routing_stats(
&self,
asn: Option<u32>,
country_code: Option<String>,
) -> Result<RoutingStatsResult, RadarError> {
self.client.get_bgp_routing_stats(asn, country_code)
}

pub fn get_prefix_origins(
&self,
origin: Option<u32>,
prefix: Option<String>,
rpki_status: Option<String>,
) -> Result<PrefixOriginsResult, RadarError> {
self.client
.get_bgp_prefix_origins(origin, prefix, rpki_status)
}
}
216 changes: 215 additions & 1 deletion src/monocle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ use chrono::DateTime;
use clap::{Args, Parser, Subcommand};
use ipnetwork::IpNetwork;
use monocle::*;
use radar_rs::RadarClient;
use rayon::prelude::*;
use serde_json::json;
use tabled::settings::{Merge, Style};
use tabled::Table;
use tabled::{Table, Tabled};
use tracing::{info, Level};

trait Validate {
Expand Down Expand Up @@ -287,6 +288,12 @@ enum Commands {
#[clap(subcommand)]
commands: RpkiCommands,
},

/// Cloudflare Radar API lookup (set CF_API_TOKEN to enable)
Radar {
#[clap(subcommand)]
commands: RadarCommands,
},
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -331,6 +338,27 @@ enum RpkiCommands {
},
}

#[derive(Subcommand)]
enum RadarCommands {
/// get routing stats
Stats {
/// a two-letter country code or asn number (e.g. US or 13335)
#[clap(name = "QUERY")]
query: String,
},

/// look up prefix to origin mapping on the most recent global routing table snapshot
Pfx2as {
/// a IP prefix or an AS number (e.g. 1.1.1.0/24 or 13335)
#[clap(name = "QUERY")]
query: String,

/// filter by RPKI validation status, valid, invalid, or unknown
#[clap(short, long)]
rpki_status: Option<String>,
},
}

fn elem_to_string(elem: &BgpElem, json: bool, pretty: bool) -> String {
if json {
let val = json!(elem);
Expand Down Expand Up @@ -761,5 +789,191 @@ fn main() {
println!("{}", Table::new(res).with(Style::markdown()));
}
},
Commands::Radar { commands } => {
let client = RadarClient::new().unwrap();

match commands {
RadarCommands::Stats { query } => {
let (country, asn) = match query.parse::<u32>() {
Ok(asn) => (None, Some(asn)),
Err(_) => (Some(query), None),
};

let res = match client.get_bgp_routing_stats(asn, country.clone()) {
Ok(res) => res,
Err(e) => {
eprintln!("unable to get routing stats: {}", e);
return;
}
};

let scope = match (country, &asn) {
(None, None) => "global".to_string(),
(Some(c), None) => c,
(None, Some(asn)) => format!("as{}", asn),
(Some(_), Some(_)) => {
eprintln!("cannot specify both country and ASN");
return;
}
};

#[derive(Tabled)]
struct Stats {
pub scope: String,
pub origins: u32,
pub prefixes: u32,
pub rpki_valid: String,
pub rpki_invalid: String,
pub rpki_unknown: String,
}
let table_data = vec![
Stats {
scope: scope.clone(),
origins: res.stats.distinct_origins,
prefixes: res.stats.distinct_prefixes,
rpki_valid: format!(
"{} ({:.2}%)",
res.stats.routes_valid,
(res.stats.routes_valid as f64 / res.stats.routes_total as f64)
* 100.0
),
rpki_invalid: format!(
"{} ({:.2}%)",
res.stats.routes_invalid,
(res.stats.routes_invalid as f64 / res.stats.routes_total as f64)
* 100.0
),
rpki_unknown: format!(
"{} ({:.2}%)",
res.stats.routes_unknown,
(res.stats.routes_unknown as f64 / res.stats.routes_total as f64)
* 100.0
),
},
Stats {
scope: format!("{} ipv4", scope),
origins: res.stats.distinct_origins_ipv4,
prefixes: res.stats.distinct_prefixes_ipv4,
rpki_valid: format!(
"{} ({:.2}%)",
res.stats.routes_valid_ipv4,
(res.stats.routes_valid_ipv4 as f64
/ res.stats.routes_total_ipv4 as f64)
* 100.0
),
rpki_invalid: format!(
"{} ({:.2}%)",
res.stats.routes_invalid_ipv4,
(res.stats.routes_invalid_ipv4 as f64
/ res.stats.routes_total_ipv4 as f64)
* 100.0
),
rpki_unknown: format!(
"{} ({:.2}%)",
res.stats.routes_unknown_ipv4,
(res.stats.routes_unknown_ipv4 as f64
/ res.stats.routes_total_ipv4 as f64)
* 100.0
),
},
Stats {
scope: format!("{} ipv6", scope),
origins: res.stats.distinct_origins_ipv6,
prefixes: res.stats.distinct_prefixes_ipv6,
rpki_valid: format!(
"{} ({:.2}%)",
res.stats.routes_valid_ipv6,
(res.stats.routes_valid_ipv6 as f64
/ res.stats.routes_total_ipv6 as f64)
* 100.0
),
rpki_invalid: format!(
"{} ({:.2}%)",
res.stats.routes_invalid_ipv6,
(res.stats.routes_invalid_ipv6 as f64
/ res.stats.routes_total_ipv6 as f64)
* 100.0
),
rpki_unknown: format!(
"{} ({:.2}%)",
res.stats.routes_unknown_ipv6,
(res.stats.routes_unknown_ipv6 as f64
/ res.stats.routes_total_ipv6 as f64)
* 100.0
),
},
];
println!("{}", Table::new(table_data).with(Style::modern()));
println!("\nData generated at {} UTC.", res.meta.data_time);
}
RadarCommands::Pfx2as { query, rpki_status } => {
let (asn, prefix) = match query.parse::<u32>() {
Ok(asn) => (Some(asn), None),
Err(_) => (None, Some(query)),
};

let rpki = if let Some(rpki_status) = rpki_status {
match rpki_status.to_lowercase().as_str() {
"valid" | "invalid" | "unknown" => Some(rpki_status),
_ => {
eprintln!("invalid rpki status: {}", rpki_status);
return;
}
}
} else {
None
};

let res = match client.get_bgp_prefix_origins(asn, prefix, rpki) {
Ok(res) => res,
Err(e) => {
eprintln!("unable to get prefix origins: {}", e);
return;
}
};

#[derive(Tabled)]
struct Pfx2origin {
pub prefix: String,
pub origin: String,
pub rpki: String,
pub visibility: String,
}

if res.prefix_origins.is_empty() {
println!("no prefix origins found for the given query");
return;
}

fn count_to_visibility(count: u32, total: u32) -> String {
let ratio = count as f64 / total as f64;
if ratio > 0.8 {
format!("high ({:.2}%)", ratio * 100.0)
} else if ratio < 0.2 {
format!("low ({:.2}%)", ratio * 100.0)
} else {
format!("mid ({:.2}%)", ratio * 100.0)
}
}

let table_data = res
.prefix_origins
.into_iter()
.map(|entry| Pfx2origin {
prefix: entry.prefix,
origin: format!("as{}", entry.origin),
rpki: entry.rpki_validation.to_lowercase(),
visibility: count_to_visibility(
entry.peer_count as u32,
res.meta.total_peers as u32,
),
})
.collect::<Vec<Pfx2origin>>();

println!("{}", Table::new(table_data).with(Style::modern()));
println!("\nData generated at {} UTC.", res.meta.data_time);
}
}
}
}
}

0 comments on commit 87b2d1a

Please sign in to comment.