diff --git a/src/config.rs b/src/config.rs index 95570c5f0..d4501ff22 100644 --- a/src/config.rs +++ b/src/config.rs @@ -94,6 +94,9 @@ const DEFAULT_TUI_PRESERVE_SCREEN: bool = false; /// The default value for `tui-as-mode`. const DEFAULT_TUI_AS_MODE: AsMode = AsMode::Asn; +/// The default value for `tui-geoip-mode`. +const DEFAULT_TUI_GEOIP_MODE: GeoIpMode = GeoIpMode::Short; + /// The default value for `tui-address-mode`. const DEFAULT_TUI_ADDRESS_MODE: AddressMode = AddressMode::Host; @@ -200,6 +203,34 @@ pub enum AsMode { Name, } +/// How to render `GeoIp` information in the hop table. +/// +/// Note that the hop details view is always shown using the `Long` representation. +#[derive(Debug, Copy, Clone, ValueEnum, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum GeoIpMode { + /// Do not display GeoIp data. + Off, + /// Show short format. + /// + /// The `city` name is shown, `subdivision` and `country` codes are shown, `continent` is not displayed. + /// + /// For example: + /// + /// `Los Angeles, CA, US` + Short, + /// Show long format. + /// + /// The `city`, `subdivision`, `country` and `continent` names are shown. + /// + /// `Los Angeles, California, United States, North America` + Long, + /// Show latitude and Longitude format. + /// + /// `lat=34.0544, long=-118.2441` + Location, +} + /// How DNS queries will be resolved. #[derive(Debug, Copy, Clone, ValueEnum, Deserialize)] #[serde(rename_all = "kebab-case")] @@ -223,7 +254,7 @@ pub struct Args { pub targets: Vec, /// Config file - #[arg(value_enum, short = 'c', long, display_order = 0)] + #[arg(value_enum, short = 'c', long, display_order = 0, value_hint = clap::ValueHint::FilePath)] pub config_file: Option, /// Output mode [default: tui] @@ -344,44 +375,52 @@ pub struct Args { #[arg(value_enum, long, display_order = 27)] pub tui_as_mode: Option, + /// How to render GeoIp information [default: compact] + #[arg(value_enum, long, display_order = 28)] + pub tui_geoip_mode: Option, + /// The maximum number of addresses to show per hop [default: auto] - #[arg(short = 'M', long, display_order = 28)] + #[arg(short = 'M', long, display_order = 29)] pub tui_max_addrs: Option, /// The maximum number of samples to record per hop [default: 256] - #[arg(long, short = 's', display_order = 29)] + #[arg(long, short = 's', display_order = 30)] pub tui_max_samples: Option, /// Preserve the screen on exit [default: false] - #[arg(long, display_order = 30)] + #[arg(long, display_order = 31)] pub tui_preserve_screen: Option, /// The Tui refresh rate [default: 100ms] - #[arg(long, display_order = 31)] + #[arg(long, display_order = 32)] pub tui_refresh_rate: Option, /// The TUI theme colors [item=color,item=color,..] - #[arg(long, value_delimiter(','), value_parser = parse_tui_theme_color_value, display_order = 32)] + #[arg(long, value_delimiter(','), value_parser = parse_tui_theme_color_value, display_order = 33)] pub tui_theme_colors: Vec<(TuiThemeItem, TuiColor)>, /// Print all TUI theme items and exit - #[arg(long, display_order = 33)] + #[arg(long, display_order = 34)] pub print_tui_theme_items: bool, /// The TUI key bindings [command=key,command=key,..] - #[arg(long, value_delimiter(','), value_parser = parse_tui_binding_value, display_order = 34)] + #[arg(long, value_delimiter(','), value_parser = parse_tui_binding_value, display_order = 35)] pub tui_key_bindings: Vec<(TuiCommandItem, TuiKeyBinding)>, /// Print all TUI commands that can be bound and exit - #[arg(long, display_order = 35)] + #[arg(long, display_order = 36)] pub print_tui_binding_commands: bool, /// The number of report cycles to run [default: 10] - #[arg(short = 'C', long, display_order = 36)] + #[arg(short = 'C', long, display_order = 37)] pub report_cycles: Option, + /// The MaxMind City GeoLite2 mmdb file + #[arg(short = 'G', long, display_order = 38, value_hint = clap::ValueHint::FilePath)] + pub geoip_mmdb_file: Option, + /// Generate shell completion - #[arg(long, display_order = 37)] + #[arg(long, display_order = 39)] pub generate: Option, } @@ -431,11 +470,13 @@ pub struct TrippyConfig { pub tui_refresh_rate: Duration, pub tui_address_mode: AddressMode, pub tui_as_mode: AsMode, + pub tui_geoip_mode: GeoIpMode, pub tui_max_addrs: Option, pub tui_theme: TuiTheme, pub tui_bindings: TuiBindings, pub mode: Mode, pub report_cycles: usize, + pub geoip_mmdb_file: Option, pub max_rounds: Option, } @@ -1121,8 +1162,8 @@ pub enum TuiCommandItem { pub mod config_file { use crate::config::{ - AddressFamily, AddressMode, AsMode, DnsResolveMethod, Mode, MultipathStrategyConfig, - Protocol, TuiColor, TuiKeyBinding, + AddressFamily, AddressMode, AsMode, DnsResolveMethod, GeoIpMode, Mode, + MultipathStrategyConfig, Protocol, TuiColor, TuiKeyBinding, }; use anyhow::Context; use serde::Deserialize; @@ -1261,7 +1302,9 @@ pub mod config_file { pub tui_refresh_rate: Option, pub tui_address_mode: Option, pub tui_as_mode: Option, + pub tui_geoip_mode: Option, pub tui_max_addrs: Option, + pub geoip_mmdb_file: Option, } #[derive(Debug, Default, Deserialize)] @@ -1445,6 +1488,11 @@ impl TryFrom<(Args, u16)> for TrippyConfig { cfg_file_tui.tui_as_mode, DEFAULT_TUI_AS_MODE, ); + let tui_geoip_mode = cfg_layer( + args.tui_geoip_mode, + cfg_file_tui.tui_geoip_mode, + DEFAULT_TUI_GEOIP_MODE, + ); let tui_max_addrs = cfg_layer_opt(args.tui_max_addrs, cfg_file_tui.tui_max_addrs); let dns_resolve_method = cfg_layer( args.dns_resolve_method, @@ -1466,6 +1514,7 @@ impl TryFrom<(Args, u16)> for TrippyConfig { cfg_file_report.report_cycles, DEFAULT_REPORT_CYCLES, ); + let geoip_mmdb_file = cfg_layer_opt(args.geoip_mmdb_file, cfg_file_tui.geoip_mmdb_file); let protocol = match (args.udp, args.tcp, protocol) { (false, false, Protocol::Icmp) => TracerProtocol::Icmp, (false, false, Protocol::Udp) | (true, _, _) => TracerProtocol::Udp, @@ -1540,6 +1589,7 @@ impl TryFrom<(Args, u16)> for TrippyConfig { validate_tui_refresh_rate(tui_refresh_rate)?; validate_report_cycles(report_cycles)?; validate_dns(dns_resolve_method, dns_lookup_as_info)?; + validate_geoip(tui_geoip_mode, &geoip_mmdb_file)?; let tui_theme_items = args .tui_theme_colors .into_iter() @@ -1578,11 +1628,13 @@ impl TryFrom<(Args, u16)> for TrippyConfig { tui_refresh_rate, tui_address_mode, tui_as_mode, + tui_geoip_mode, tui_max_addrs, tui_theme, tui_bindings, mode, report_cycles, + geoip_mmdb_file, max_rounds, }) } @@ -1760,6 +1812,23 @@ fn validate_dns( } } +fn validate_geoip( + tui_geoip_mode: GeoIpMode, + geoip_mmdb_file: &Option, +) -> anyhow::Result<()> { + if matches!( + tui_geoip_mode, + GeoIpMode::Short | GeoIpMode::Long | GeoIpMode::Location + ) && geoip_mmdb_file.is_none() + { + Err(anyhow!( + "geoip_mmdb_file must be given for tui_geoip_mode of `{tui_geoip_mode:?}`" + )) + } else { + Ok(()) + } +} + /// Validate key bindings. fn validate_bindings(bindings: &TuiBindings) -> anyhow::Result<()> { let duplicates = bindings.find_duplicates(); diff --git a/src/frontend.rs b/src/frontend.rs index 22f5b3a9b..ea7deebab 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -1,8 +1,10 @@ use crate::backend::Hop; use crate::config::{ - AddressMode, AsMode, DnsResolveMethod, TuiBindings, TuiColor, TuiKeyBinding, TuiTheme, + AddressMode, AsMode, DnsResolveMethod, GeoIpMode, TuiBindings, TuiColor, TuiKeyBinding, + TuiTheme, }; use crate::dns::{AsInfo, DnsEntry, Resolved, Unresolved}; +use crate::geoip::{GeoIpCity, GeoIpLookup}; use crate::{DnsResolver, Trace, TraceInfo}; use chrono::SecondsFormat; use crossterm::event::{KeyEvent, KeyModifiers}; @@ -15,6 +17,7 @@ use itertools::Itertools; use std::collections::BTreeMap; use std::io; use std::net::IpAddr; +use std::rc::Rc; use std::time::{Duration, SystemTime}; use trippy::tracing::{PortDirection, TracerProtocol}; use tui::layout::{Alignment, Direction, Rect}; @@ -280,8 +283,10 @@ pub struct TuiConfig { address_mode: AddressMode, /// Lookup `AS` information. lookup_as_info: bool, - /// The to render AS data. + /// How to render AS data. as_mode: AsMode, + /// How to render GeoIp data. + geoip_mode: GeoIpMode, /// The maximum number of addresses to show per hop. max_addrs: Option, /// The maximum number of samples to record per hop. @@ -300,6 +305,7 @@ impl TuiConfig { address_mode: AddressMode, lookup_as_info: bool, as_mode: AsMode, + geoip_mode: GeoIpMode, max_addrs: Option, max_samples: usize, tui_theme: TuiTheme, @@ -311,6 +317,7 @@ impl TuiConfig { address_mode, lookup_as_info, as_mode, + geoip_mode, max_addrs, max_samples, theme: Theme::from(tui_theme), @@ -330,6 +337,7 @@ struct TuiApp { /// Only used in detail mode. selected_hop_address: usize, resolver: DnsResolver, + geoip_lookup: GeoIpLookup, show_help: bool, show_hop_details: bool, show_chart: bool, @@ -338,7 +346,12 @@ struct TuiApp { } impl TuiApp { - fn new(tui_config: TuiConfig, resolver: DnsResolver, trace_info: Vec) -> Self { + fn new( + tui_config: TuiConfig, + resolver: DnsResolver, + geoip_lookup: GeoIpLookup, + trace_info: Vec, + ) -> Self { Self { selected_tracer_data: Trace::new(tui_config.max_samples), trace_info, @@ -347,6 +360,7 @@ impl TuiApp { trace_selected: 0, selected_hop_address: 0, resolver, + geoip_lookup, show_help: false, show_hop_details: false, show_chart: false, @@ -550,6 +564,7 @@ pub fn run_frontend( traces: Vec, tui_config: TuiConfig, resolver: DnsResolver, + geoip_lookup: GeoIpLookup, ) -> anyhow::Result<()> { enable_raw_mode()?; let mut stdout = io::stdout(); @@ -557,7 +572,7 @@ pub fn run_frontend( let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; let preserve_screen = tui_config.preserve_screen; - let res = run_app(&mut terminal, traces, tui_config, resolver); + let res = run_app(&mut terminal, traces, tui_config, resolver, geoip_lookup); disable_raw_mode()?; if !preserve_screen { execute!(terminal.backend_mut(), LeaveAlternateScreen)?; @@ -574,8 +589,9 @@ fn run_app( trace_info: Vec, tui_config: TuiConfig, resolver: DnsResolver, + geoip_lookup: GeoIpLookup, ) -> io::Result<()> { - let mut app = TuiApp::new(tui_config, resolver, trace_info); + let mut app = TuiApp::new(tui_config, resolver, geoip_lookup, trace_info); loop { if app.frozen_start.is_none() { app.snapshot_trace_data(); @@ -1057,11 +1073,10 @@ fn render_splash(f: &mut Frame<'_, B>, app: &mut TuiApp, rect: Rect) fn render_table(f: &mut Frame<'_, B>, app: &mut TuiApp, rect: Rect) { let header = render_table_header(app.tui_config.theme); let selected_style = Style::default().add_modifier(Modifier::REVERSED); - let rows = app - .tracer_data() - .hops() - .iter() - .map(|hop| render_table_row(app, hop, &app.resolver, &app.tui_config)); + let rows = + app.tracer_data().hops().iter().map(|hop| { + render_table_row(app, hop, &app.resolver, &app.geoip_lookup, &app.tui_config) + }); let table = Table::new(rows) .header(header) .block( @@ -1097,6 +1112,7 @@ fn render_table_row( app: &TuiApp, hop: &Hop, dns: &DnsResolver, + geoip_lookup: &GeoIpLookup, config: &TuiConfig, ) -> Row<'static> { let is_selected_hop = app @@ -1107,9 +1123,9 @@ fn render_table_row( let is_in_round = app.tracer_data().is_in_round(hop); let ttl_cell = render_ttl_cell(hop); let (hostname_cell, row_height) = if is_selected_hop && app.show_hop_details { - render_hostname_with_details(app, hop, dns, config) + render_hostname_with_details(app, hop, dns, geoip_lookup, config) } else { - render_hostname(hop, dns, config) + render_hostname(hop, dns, geoip_lookup, config) }; let loss_pct_cell = render_loss_pct_cell(hop); let total_sent_cell = render_total_sent_cell(hop); @@ -1212,13 +1228,18 @@ fn render_status_cell(hop: &Hop, is_target: bool) -> Cell<'static> { } /// Render hostname table cell (normal mode). -fn render_hostname(hop: &Hop, dns: &DnsResolver, config: &TuiConfig) -> (Cell<'static>, u16) { +fn render_hostname( + hop: &Hop, + dns: &DnsResolver, + geoip_lookup: &GeoIpLookup, + config: &TuiConfig, +) -> (Cell<'static>, u16) { let (hostname, count) = if hop.total_recv() > 0 { match config.max_addrs { None => { let hostnames = hop .addrs_with_counts() - .map(|(addr, &freq)| format_address(addr, freq, hop, dns, config)) + .map(|(addr, &freq)| format_address(addr, freq, hop, dns, geoip_lookup, config)) .join("\n"); let count = hop.addr_count().clamp(1, u8::MAX as usize); (hostnames, count as u16) @@ -1229,7 +1250,7 @@ fn render_hostname(hop: &Hop, dns: &DnsResolver, config: &TuiConfig) -> (Cell<'s .sorted_unstable_by_key(|(_, &cnt)| cnt) .rev() .take(max_addr as usize) - .map(|(addr, &freq)| format_address(addr, freq, hop, dns, config)) + .map(|(addr, &freq)| format_address(addr, freq, hop, dns, geoip_lookup, config)) .join("\n"); let count = hop.addr_count().clamp(1, max_addr as usize); (hostnames, count as u16) @@ -1247,6 +1268,7 @@ fn format_address( freq: usize, hop: &Hop, dns: &DnsResolver, + geoip_lookup: &GeoIpLookup, config: &TuiConfig, ) -> String { let addr_fmt = match config.address_mode { @@ -1271,14 +1293,41 @@ fn format_address( format!("{hostname} ({addr})") } }; - if hop.addr_count() > 1 { - format!( - "{} [{:.1}%]", - addr_fmt, - (freq as f64 / hop.total_recv() as f64) * 100_f64 - ) - } else { - addr_fmt + let geo_fmt = match config.geoip_mode { + GeoIpMode::Off => None, + GeoIpMode::Short => geoip_lookup + .lookup(*addr) + .unwrap_or_default() + .map(|geo| geo.short_name()), + GeoIpMode::Long => geoip_lookup + .lookup(*addr) + .unwrap_or_default() + .map(|geo| geo.long_name()), + GeoIpMode::Location => geoip_lookup + .lookup(*addr) + .unwrap_or_default() + .map(|geo| geo.location()), + }; + match geo_fmt { + Some(geo) if hop.addr_count() > 1 => { + format!( + "{} [{}] [{:.1}%]", + addr_fmt, + geo, + (freq as f64 / hop.total_recv() as f64) * 100_f64 + ) + } + Some(geo) => { + format!("{addr_fmt} [{geo}]") + } + None if hop.addr_count() > 1 => { + format!( + "{} [{:.1}%]", + addr_fmt, + (freq as f64 / hop.total_recv() as f64) * 100_f64 + ) + } + None => addr_fmt, } } @@ -1323,11 +1372,12 @@ fn render_hostname_with_details( app: &TuiApp, hop: &Hop, dns: &DnsResolver, + geoip_lookup: &GeoIpLookup, config: &TuiConfig, ) -> (Cell<'static>, u16) { let (rendered, count) = if hop.total_recv() > 0 { let index = app.selected_hop_address; - format_details(hop, index, dns, config) + format_details(hop, index, dns, geoip_lookup, config) } else { (String::from("No response"), 1) }; @@ -1340,6 +1390,7 @@ fn format_details( hop: &Hop, offset: usize, dns: &DnsResolver, + geoip_lookup: &GeoIpLookup, config: &TuiConfig, ) -> (String, u16) { let Some(addr) = hop.addrs().nth(offset) else { @@ -1347,20 +1398,24 @@ fn format_details( }; let count = hop.addr_count(); let index = offset + 1; + let geoip = geoip_lookup.lookup(*addr).unwrap_or_default(); + if config.lookup_as_info { let dns_entry = dns.reverse_lookup_with_asinfo(*addr); match dns_entry { DnsEntry::Pending(addr) => { - let details = fmt_details_with_asn(addr, index, count, None, None); - (details, 4) + let details = fmt_details_with_asn(addr, index, count, None, None, geoip); + (details, 5) } DnsEntry::Resolved(Resolved::WithAsInfo(addr, hosts, asinfo)) => { - let details = fmt_details_with_asn(addr, index, count, Some(hosts), Some(asinfo)); - (details, 4) + let details = + fmt_details_with_asn(addr, index, count, Some(hosts), Some(asinfo), geoip); + (details, 5) } DnsEntry::NotFound(Unresolved::WithAsInfo(addr, asinfo)) => { - let details = fmt_details_with_asn(addr, index, count, Some(vec![]), Some(asinfo)); - (details, 4) + let details = + fmt_details_with_asn(addr, index, count, Some(vec![]), Some(asinfo), geoip); + (details, 5) } DnsEntry::Failed(ip) => { let details = format!("Failed: {ip}"); @@ -1377,15 +1432,15 @@ fn format_details( let dns_entry = dns.reverse_lookup(*addr); match dns_entry { DnsEntry::Pending(addr) => { - let details = fmt_details_no_asn(addr, index, count, None); + let details = fmt_details_no_asn(addr, index, count, None, geoip); (details, 2) } DnsEntry::Resolved(Resolved::Normal(addr, hosts)) => { - let details = fmt_details_no_asn(addr, index, count, Some(hosts)); + let details = fmt_details_no_asn(addr, index, count, Some(hosts), geoip); (details, 2) } DnsEntry::NotFound(Unresolved::Normal(addr)) => { - let details = fmt_details_no_asn(addr, index, count, Some(vec![])); + let details = fmt_details_no_asn(addr, index, count, Some(vec![]), geoip); (details, 2) } DnsEntry::Failed(ip) => { @@ -1421,6 +1476,7 @@ fn fmt_details_with_asn( count: usize, hostnames: Option>, asinfo: Option, + geoip: Option>, ) -> String { let as_formatted = if let Some(info) = asinfo { if info.asn.is_empty() { @@ -1443,7 +1499,12 @@ fn fmt_details_with_asn( } else { "Host: ".to_string() }; - format!("{addr} [{index} of {count}]\n{hosts_rendered}\n{as_formatted}") + let geoip_formatted = if let Some(geo) = geoip { + format!("Geo: {}", geo.long_name()) + } else { + "Geo: ".to_string() + }; + format!("{addr} [{index} of {count}]\n{hosts_rendered}\n{as_formatted}\n{geoip_formatted}") } /// Format hostname details without AS information. @@ -1462,6 +1523,7 @@ fn fmt_details_no_asn( index: usize, count: usize, hostnames: Option>, + geoip: Option>, ) -> String { let hosts_rendered = if let Some(hosts) = hostnames { if hosts.is_empty() { @@ -1472,7 +1534,12 @@ fn fmt_details_no_asn( } else { "Host: ".to_string() }; - format!("{addr} [{index} of {count}]\n{hosts_rendered}") + let geoip_formatted = if let Some(geo) = geoip { + format!("Geo: {}", geo.long_name()) + } else { + "Geo: ".to_string() + }; + format!("{addr} [{index} of {count}]\n{hosts_rendered}\n{geoip_formatted}") } /// Render the footer. diff --git a/src/geoip.rs b/src/geoip.rs index e69de29bb..ff809bb76 100644 --- a/src/geoip.rs +++ b/src/geoip.rs @@ -0,0 +1,164 @@ +use anyhow::Context; +use itertools::Itertools; +use maxminddb::geoip2::City; +use maxminddb::Reader; +use std::cell::RefCell; +use std::collections::HashMap; +use std::net::IpAddr; +use std::path::Path; +use std::rc::Rc; + +#[derive(Debug, Clone, Default)] +pub struct GeoIpCity { + latitude: Option, + longitude: Option, + city: Option, + subdivision: Option, + subdivision_code: Option, + country: Option, + country_code: Option, + continent: Option, +} + +impl GeoIpCity { + pub fn short_name(&self) -> String { + [ + self.city.as_ref(), + self.subdivision_code.as_ref(), + self.country_code.as_ref(), + ] + .into_iter() + .flatten() + .join(", ") + } + + pub fn long_name(&self) -> String { + [ + self.city.as_ref(), + self.subdivision.as_ref(), + self.country.as_ref(), + self.continent.as_ref(), + ] + .into_iter() + .flatten() + .join(", ") + } + pub fn location(&self) -> String { + format!( + "lat={}, long={}", + self.latitude.unwrap_or_default(), + self.longitude.unwrap_or_default() + ) + } +} + +impl From> for GeoIpCity { + fn from(value: City<'_>) -> Self { + let city = value + .city + .as_ref() + .and_then(|city| city.names.as_ref()) + .and_then(|names| names.get(LOCALE)) + .map(ToString::to_string); + let subdivision = value + .subdivisions + .as_ref() + .and_then(|c| c.first()) + .and_then(|c| c.names.as_ref()) + .and_then(|names| names.get(LOCALE)) + .map(ToString::to_string); + let subdivision_code = value + .subdivisions + .as_ref() + .and_then(|c| c.first()) + .and_then(|c| c.iso_code.as_ref()) + .map(ToString::to_string); + let country = value + .country + .as_ref() + .and_then(|country| country.names.as_ref()) + .and_then(|names| names.get(LOCALE)) + .map(ToString::to_string); + let country_code = value + .country + .as_ref() + .and_then(|country| country.iso_code.as_ref()) + .map(ToString::to_string); + let continent = value + .continent + .as_ref() + .and_then(|continent| continent.names.as_ref()) + .and_then(|names| names.get(LOCALE)) + .map(ToString::to_string); + let latitude = value + .location + .as_ref() + .and_then(|location| location.latitude); + let longitude = value + .location + .as_ref() + .and_then(|location| location.longitude); + Self { + latitude, + longitude, + city, + subdivision, + subdivision_code, + country, + country_code, + continent, + } + } +} + +/// The default locale. +const LOCALE: &str = "en"; + +/// Alias for a cache of `GeoIp` data. +type Cache = RefCell>>; + +/// Lookup `GeoIpCity` data form an `IpAddr`. +#[derive(Debug)] +pub struct GeoIpLookup { + reader: Option>>, + cache: Cache, +} + +impl GeoIpLookup { + /// Create a new `GeoIpLookup` from a `MaxMind` DB file. + pub fn from_file>(path: P) -> anyhow::Result { + let reader = maxminddb::Reader::open_readfile(path.as_ref()) + .context(format!("{}", path.as_ref().display()))?; + Ok(Self { + reader: Some(reader), + cache: RefCell::new(HashMap::new()), + }) + } + + /// Create a `GeoIpLookup` that returns `None` for all `IpAddr` lookups. + pub fn empty() -> Self { + Self { + reader: None, + cache: RefCell::new(HashMap::new()), + } + } + + /// Lookup an `GeoIpCity` for an `IpAddr`. + /// + /// If an entry is found it is cached and returned, otherwise None is returned. + pub fn lookup(&self, addr: IpAddr) -> anyhow::Result>> { + if let Some(reader) = &self.reader { + if let Some(geo) = self.cache.borrow().get(&addr).map(Clone::clone) { + return Ok(Some(geo)); + } + let city_data = reader.lookup::>(addr)?; + let geo = self + .cache + .borrow_mut() + .insert(addr, Rc::new(GeoIpCity::from(city_data))); + Ok(geo) + } else { + Ok(None) + } + } +} diff --git a/src/main.rs b/src/main.rs index c44dbb204..42d4ab964 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ use crate::caps::{drop_caps, ensure_caps}; use crate::config::{Mode, TrippyConfig}; use crate::dns::{DnsResolver, DnsResolverConfig}; use crate::frontend::TuiConfig; +use crate::geoip::GeoIpLookup; use anyhow::{anyhow, Error}; use clap::Parser; use config::Args; @@ -36,12 +37,14 @@ mod caps; mod config; mod dns; mod frontend; +mod geoip; mod report; fn main() -> anyhow::Result<()> { let pid = u16::try_from(std::process::id() % u32::from(u16::MAX))?; let cfg = TrippyConfig::try_from((Args::parse(), pid))?; let resolver = start_dns_resolver(&cfg)?; + let geoip_lookup = create_geoip_lookup(&cfg)?; ensure_caps()?; let traces: Vec<_> = cfg .targets @@ -50,7 +53,7 @@ fn main() -> anyhow::Result<()> { .map(|(i, target_host)| start_tracer(&cfg, target_host, pid + i as u16, &resolver)) .collect::>>()?; drop_caps()?; - run_frontend(&cfg, resolver, traces)?; + run_frontend(&cfg, resolver, geoip_lookup, traces)?; Ok(()) } @@ -68,6 +71,14 @@ fn start_dns_resolver(cfg: &TrippyConfig) -> anyhow::Result { }) } +fn create_geoip_lookup(cfg: &TrippyConfig) -> anyhow::Result { + if let Some(path) = cfg.geoip_mmdb_file.as_ref() { + GeoIpLookup::from_file(path) + } else { + Ok(GeoIpLookup::empty()) + } +} + /// Start a tracer to a given target. fn start_tracer( cfg: &TrippyConfig, @@ -121,10 +132,11 @@ fn start_tracer( fn run_frontend( args: &TrippyConfig, resolver: DnsResolver, + geoip_lookup: GeoIpLookup, traces: Vec, ) -> anyhow::Result<()> { match args.mode { - Mode::Tui => frontend::run_frontend(traces, make_tui_config(args), resolver)?, + Mode::Tui => frontend::run_frontend(traces, make_tui_config(args), resolver, geoip_lookup)?, Mode::Stream => report::run_report_stream(&traces[0])?, Mode::Csv => report::run_report_csv(&traces[0], args.report_cycles, &resolver)?, Mode::Json => report::run_report_json(&traces[0], args.report_cycles, &resolver)?, @@ -211,6 +223,7 @@ fn make_tui_config(args: &TrippyConfig) -> TuiConfig { args.tui_address_mode, args.dns_lookup_as_info, args.tui_as_mode, + args.tui_geoip_mode, args.tui_max_addrs, args.tui_max_samples, args.tui_theme, diff --git a/trippy-config-sample.toml b/trippy-config-sample.toml index cfd41c7bf..18401b7df 100644 --- a/trippy-config-sample.toml +++ b/trippy-config-sample.toml @@ -204,6 +204,15 @@ tui-address-mode = "host" # name - Display the AS name tui-as-mode = "asn" +# How to render GeoIp information. +# +# Allowed values are: +# off - Do not show GeoIp information +# short - Show short format GeoIp information +# long - Show long format GeoIp information +# location - Show latitude and Longitude format GeoIp information +tui-geoip-mode = "short" + # The maximum number of addresses to show per hop [default: auto] # # Use a zero value for `auto`.