Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add Cloudflare Radar API queries support #50

Merged
merged 1 commit into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
}
}
}