diff --git a/src/config.rs b/src/config.rs index 10d5cfb6..c24d94cf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -117,6 +117,25 @@ pub enum AsMode { Name, } +/// How to render `icmp` extensions in the hops table. +#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum IcmpExtensionMode { + /// Do not show `icmp` extensions. + Off, + /// Show MPLS label(s) only. + Mpls, + /// Show full `icmp` extension data for all known extensions. + /// + /// For MPLS the fields shown are `label`, `ttl`, `exp` & `bos`. + Full, + /// Show full `icmp` extension data for all known and unknown classes. + /// + /// This is the same as `Full`, but also shows `class`, `subtype` and + /// `object` for unknown extensions. + All, +} + /// How to render `GeoIp` information in the hop table. /// /// Note that the hop details view is always shown using the `Long` representation. @@ -217,6 +236,7 @@ pub struct TrippyConfig { pub tui_privacy_max_ttl: u8, pub tui_address_mode: AddressMode, pub tui_as_mode: AsMode, + pub tui_icmp_extension_mode: IcmpExtensionMode, pub tui_geoip_mode: GeoIpMode, pub tui_max_addrs: Option, pub tui_theme: TuiTheme, @@ -418,6 +438,13 @@ impl TrippyConfig { cfg_file_tui.tui_as_mode, constants::DEFAULT_TUI_AS_MODE, ); + + let tui_icmp_extension_mode = cfg_layer( + args.tui_icmp_extension_mode, + cfg_file_tui.tui_icmp_extension_mode, + constants::DEFAULT_TUI_ICMP_EXTENSION_MODE, + ); + let tui_geoip_mode = cfg_layer( args.tui_geoip_mode, cfg_file_tui.tui_geoip_mode, @@ -577,6 +604,7 @@ impl TrippyConfig { tui_privacy_max_ttl, tui_address_mode, tui_as_mode, + tui_icmp_extension_mode, tui_geoip_mode, tui_max_addrs, tui_theme, @@ -626,6 +654,7 @@ impl Default for TrippyConfig { tui_privacy_max_ttl: constants::DEFAULT_TUI_PRIVACY_MAX_TTL, tui_address_mode: constants::DEFAULT_TUI_ADDRESS_MODE, tui_as_mode: constants::DEFAULT_TUI_AS_MODE, + tui_icmp_extension_mode: constants::DEFAULT_TUI_ICMP_EXTENSION_MODE, tui_geoip_mode: constants::DEFAULT_TUI_GEOIP_MODE, tui_max_addrs: None, tui_theme: TuiTheme::default(), diff --git a/src/config/cmd.rs b/src/config/cmd.rs index 43e50573..74ec8f9b 100644 --- a/src/config/cmd.rs +++ b/src/config/cmd.rs @@ -1,8 +1,8 @@ use crate::config::binding::TuiCommandItem; use crate::config::theme::TuiThemeItem; use crate::config::{ - AddressMode, AsMode, DnsResolveMethodConfig, GeoIpMode, LogFormat, LogSpanEvents, Mode, - MultipathStrategyConfig, Protocol, TuiColor, TuiKeyBinding, + AddressMode, AsMode, DnsResolveMethodConfig, GeoIpMode, IcmpExtensionMode, LogFormat, + LogSpanEvents, Mode, MultipathStrategyConfig, Protocol, TuiColor, TuiKeyBinding, }; use anyhow::anyhow; use clap::builder::Styles; @@ -161,6 +161,10 @@ pub struct Args { #[arg(value_enum, long)] pub tui_as_mode: Option, + /// How to render ICMP extensions [default: mpls] + #[arg(value_enum, long)] + pub tui_icmp_extension_mode: Option, + /// How to render GeoIp information [default: short] #[arg(value_enum, long)] pub tui_geoip_mode: Option, diff --git a/src/config/constants.rs b/src/config/constants.rs index 3df193c3..68398d84 100644 --- a/src/config/constants.rs +++ b/src/config/constants.rs @@ -1,6 +1,6 @@ use crate::config::{ - AddressFamily, AddressMode, AsMode, DnsResolveMethodConfig, GeoIpMode, LogFormat, - LogSpanEvents, Mode, MultipathStrategyConfig, Protocol, + AddressFamily, AddressMode, AsMode, DnsResolveMethodConfig, GeoIpMode, IcmpExtensionMode, + LogFormat, LogSpanEvents, Mode, MultipathStrategyConfig, Protocol, }; use std::time::Duration; @@ -82,6 +82,9 @@ pub const DEFAULT_TUI_PRESERVE_SCREEN: bool = false; /// The default value for `tui-as-mode`. pub const DEFAULT_TUI_AS_MODE: AsMode = AsMode::Asn; +/// The default value for `tui-icmp-extension-mode`. +pub const DEFAULT_TUI_ICMP_EXTENSION_MODE: IcmpExtensionMode = IcmpExtensionMode::Off; + /// The default value for `tui-geoip-mode`. pub const DEFAULT_TUI_GEOIP_MODE: GeoIpMode = GeoIpMode::Off; diff --git a/src/config/file.rs b/src/config/file.rs index 1026cb46..82795561 100644 --- a/src/config/file.rs +++ b/src/config/file.rs @@ -1,8 +1,8 @@ use crate::config::binding::TuiKeyBinding; use crate::config::theme::TuiColor; use crate::config::{ - AddressFamily, AddressMode, AsMode, DnsResolveMethodConfig, GeoIpMode, LogFormat, - LogSpanEvents, Mode, MultipathStrategyConfig, Protocol, + AddressFamily, AddressMode, AsMode, DnsResolveMethodConfig, GeoIpMode, IcmpExtensionMode, + LogFormat, LogSpanEvents, Mode, MultipathStrategyConfig, Protocol, }; use anyhow::Context; use etcetera::BaseStrategy; @@ -218,6 +218,7 @@ pub struct ConfigTui { pub tui_privacy_max_ttl: Option, pub tui_address_mode: Option, pub tui_as_mode: Option, + pub tui_icmp_extension_mode: Option, pub tui_geoip_mode: Option, pub tui_max_addrs: Option, pub geoip_mmdb_file: Option, @@ -232,6 +233,7 @@ impl Default for ConfigTui { tui_privacy_max_ttl: Some(super::constants::DEFAULT_TUI_PRIVACY_MAX_TTL), tui_address_mode: Some(super::constants::DEFAULT_TUI_ADDRESS_MODE), tui_as_mode: Some(super::constants::DEFAULT_TUI_AS_MODE), + tui_icmp_extension_mode: Some(super::constants::DEFAULT_TUI_ICMP_EXTENSION_MODE), tui_geoip_mode: Some(super::constants::DEFAULT_TUI_GEOIP_MODE), tui_max_addrs: Some(super::constants::DEFAULT_TUI_MAX_ADDRS), geoip_mmdb_file: None, diff --git a/src/frontend/config.rs b/src/frontend/config.rs index 81e03b3a..0e7eec0e 100644 --- a/src/frontend/config.rs +++ b/src/frontend/config.rs @@ -1,5 +1,5 @@ -use crate::config::TuiBindings; use crate::config::{AddressMode, AsMode, GeoIpMode, TuiTheme}; +use crate::config::{IcmpExtensionMode, TuiBindings}; use crate::frontend::binding::Bindings; use crate::frontend::theme::Theme; use std::time::Duration; @@ -19,6 +19,8 @@ pub struct TuiConfig { pub lookup_as_info: bool, /// How to render AS data. pub as_mode: AsMode, + /// How to render ICMP extensions. + pub icmp_extension_mode: IcmpExtensionMode, /// How to render GeoIp data. pub geoip_mode: GeoIpMode, /// The maximum number of addresses to show per hop. @@ -40,6 +42,7 @@ impl TuiConfig { address_mode: AddressMode, lookup_as_info: bool, as_mode: AsMode, + icmp_extension_mode: IcmpExtensionMode, geoip_mode: GeoIpMode, max_addrs: Option, max_samples: usize, @@ -53,6 +56,7 @@ impl TuiConfig { address_mode, lookup_as_info, as_mode, + icmp_extension_mode, geoip_mode, max_addrs, max_samples, diff --git a/src/frontend/render/settings.rs b/src/frontend/render/settings.rs index b1da4d36..a6d01996 100644 --- a/src/frontend/render/settings.rs +++ b/src/frontend/render/settings.rs @@ -216,6 +216,7 @@ fn format_trace_settings(app: &TuiApp) -> Vec { ), SettingsItem::new("packet-size", format!("{}", cfg.packet_size)), SettingsItem::new("payload-pattern", format!("{}", cfg.payload_pattern)), + SettingsItem::new("icmp-extensions", format!("{}", cfg.icmp_extensions)), SettingsItem::new("interface", interface), SettingsItem::new("multipath-strategy", cfg.multipath_strategy.to_string()), SettingsItem::new("target-port", dst_port), @@ -419,7 +420,7 @@ fn format_theme_settings(app: &TuiApp) -> Vec { /// The name and number of items for each tabs in the setting dialog. pub const SETTINGS_TABS: [(&str, usize); 6] = [ ("Tui", 8), - ("Trace", 14), + ("Trace", 15), ("Dns", 4), ("GeoIp", 1), ("Bindings", 29), diff --git a/src/frontend/render/table.rs b/src/frontend/render/table.rs index 3fe31443..4f9499fd 100644 --- a/src/frontend/render/table.rs +++ b/src/frontend/render/table.rs @@ -1,5 +1,5 @@ use crate::backend::trace::Hop; -use crate::config::{AddressMode, AsMode, GeoIpMode}; +use crate::config::{AddressMode, AsMode, GeoIpMode, IcmpExtensionMode}; use crate::frontend::config::TuiConfig; use crate::frontend::theme::Theme; use crate::frontend::tui_app::TuiApp; @@ -12,6 +12,7 @@ use ratatui::Frame; use std::net::IpAddr; use std::rc::Rc; use trippy::dns::{AsInfo, DnsEntry, DnsResolver, Resolved, Resolver, Unresolved}; +use trippy::tracing::{Extension, Extensions, MplsLabelStackMember, UnknownExtension}; /// Render the table of data about the hops. /// @@ -259,6 +260,7 @@ fn format_address( format!("{hostname} ({addr})") } }; + let exp_fmt = format_extensions(config, hop); let geo_fmt = match config.geoip_mode { GeoIpMode::Off => None, GeoIpMode::Short => geoip_lookup @@ -274,27 +276,25 @@ fn format_address( .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, + let freq_fmt = if hop.addr_count() > 1 { + Some(format!( + "{:.1}%", + (freq as f64 / hop.total_recv() as f64) * 100_f64 + )) + } else { + None + }; + let mut address = addr_fmt; + if let Some(geo) = geo_fmt.as_deref() { + address.push_str(&format!(" [{geo}]")); + } + if let Some(exp) = exp_fmt { + address.push_str(&format!(" [{exp}]")); } + if let Some(freq) = freq_fmt { + address.push_str(&format!(" [{freq}]")); + } + address } /// Format a `DnsEntry` with or without `AS` information (if available) @@ -333,6 +333,101 @@ fn format_asinfo(asinfo: &AsInfo, as_mode: AsMode) -> String { } } +/// Format `icmp` extensions. +fn format_extensions(config: &TuiConfig, hop: &Hop) -> Option { + if let Some(extensions) = hop.extensions() { + match config.icmp_extension_mode { + IcmpExtensionMode::Off => None, + IcmpExtensionMode::Mpls => format_extensions_mpls(extensions), + IcmpExtensionMode::Full => format_extensions_full(extensions), + IcmpExtensionMode::All => Some(format_extensions_all(extensions)), + } + } else { + None + } +} + +/// Format MPLS extensions as: `labels: 12345, 6789`. +/// +/// If not MPLS extensions are present then None is returned. +fn format_extensions_mpls(extensions: &Extensions) -> Option { + let labels = extensions + .extensions + .iter() + .filter_map(|ext| match ext { + Extension::Unknown(_) => None, + Extension::Mpls(stack) => Some(stack), + }) + .flat_map(|ext| &ext.members) + .map(|mem| mem.label) + .format(", ") + .to_string(); + if labels.is_empty() { + None + } else { + Some(format!("labels: {labels}")) + } +} + +/// Format all known extensions with full details. +/// +/// For MPLS: `mpls(label=48320, ttl=1, exp=0, bos=1), mpls(...)` +fn format_extensions_full(extensions: &Extensions) -> Option { + let formatted = extensions + .extensions + .iter() + .filter_map(|ext| match ext { + Extension::Unknown(_) => None, + Extension::Mpls(stack) => Some(stack), + }) + .flat_map(|ext| &ext.members) + .map(format_ext_mpls_stack_member) + .format(", ") + .to_string(); + if formatted.is_empty() { + None + } else { + Some(formatted) + } +} + +/// Format a list all known and unknown extensions with full details. +/// +/// `mpls(label=48320, ttl=1, exp=0, bos=1), mpls(label=...), unknown(class=1, sub=1, object=0b c8 c1 01), ...` +fn format_extensions_all(extensions: &Extensions) -> String { + extensions + .extensions + .iter() + .flat_map(|ext| match ext { + Extension::Unknown(unknown) => vec![format_ext_unknown(unknown)], + Extension::Mpls(stack) => stack + .members + .iter() + .map(format_ext_mpls_stack_member) + .collect::>(), + }) + .format(", ") + .to_string() +} + +/// Format a MPLS `icmp` extension object. +pub fn format_ext_mpls_stack_member(member: &MplsLabelStackMember) -> String { + format!( + "mpls(label={}, ttl={}, exp={}, bos={})", + member.label, member.ttl, member.exp, member.bos + ) +} + +/// Format an unknown `icmp` extension object. +pub fn format_ext_unknown(unknown: &UnknownExtension) -> String { + format!( + "unknown(class={}, subtype={}, object={:02x})", + unknown.class_num, + unknown.class_subtype, + unknown.bytes.iter().format(" ") + ) +} + /// Render hostname table cell (detailed mode). fn render_hostname_with_details( app: &TuiApp, @@ -351,7 +446,7 @@ fn render_hostname_with_details( } else { String::from("No response") }; - (Cell::from(rendered), 6) + (Cell::from(rendered), 7) } /// Format hop details. @@ -373,11 +468,21 @@ fn format_details( } else { dns.lazy_reverse_lookup(*addr) }; + let ext = hop.extensions(); match dns_entry { - DnsEntry::Pending(addr) => fmt_details_line(addr, index, count, None, None, geoip, config), - DnsEntry::Resolved(Resolved::WithAsInfo(addr, hosts, asinfo)) => { - fmt_details_line(addr, index, count, Some(hosts), Some(asinfo), geoip, config) + DnsEntry::Pending(addr) => { + fmt_details_line(addr, index, count, None, None, geoip, ext, config) } + DnsEntry::Resolved(Resolved::WithAsInfo(addr, hosts, asinfo)) => fmt_details_line( + addr, + index, + count, + Some(hosts), + Some(asinfo), + geoip, + ext, + config, + ), DnsEntry::NotFound(Unresolved::WithAsInfo(addr, asinfo)) => fmt_details_line( addr, index, @@ -385,13 +490,14 @@ fn format_details( Some(vec![]), Some(asinfo), geoip, + ext, config, ), DnsEntry::Resolved(Resolved::Normal(addr, hosts)) => { - fmt_details_line(addr, index, count, Some(hosts), None, geoip, config) + fmt_details_line(addr, index, count, Some(hosts), None, geoip, ext, config) } DnsEntry::NotFound(Unresolved::Normal(addr)) => { - fmt_details_line(addr, index, count, Some(vec![]), None, geoip, config) + fmt_details_line(addr, index, count, Some(vec![]), None, geoip, ext, config) } DnsEntry::Failed(ip) => { format!("Failed: {ip}") @@ -413,7 +519,9 @@ fn format_details( /// AS Info: 142.250.0.0/15 arin 2012-05-24 /// Geo: United States, North America /// Pos: 37.751, -97.822 (~1000km) +/// Ext: [mpls(label=48268, ttl=1, exp=0, bos=1)] /// ``` +#[allow(clippy::too_many_arguments)] fn fmt_details_line( addr: IpAddr, index: usize, @@ -421,6 +529,7 @@ fn fmt_details_line( hostnames: Option>, asinfo: Option, geoip: Option>, + extensions: Option<&Extensions>, config: &TuiConfig, ) -> String { let as_formatted = match (config.lookup_as_info, asinfo) { @@ -455,7 +564,12 @@ fn fmt_details_line( } else { "Geo: \nPos: ".to_string() }; - format!("{addr} [{index} of {count}]\n{hosts_rendered}\n{as_formatted}\n{geoip_formatted}") + let ext_formatted = if let Some(extensions) = extensions { + format!("Ext: [{}]", format_extensions_all(extensions)) + } else { + "Ext: ".to_string() + }; + format!("{addr} [{index} of {count}]\n{hosts_rendered}\n{as_formatted}\n{geoip_formatted}\n{ext_formatted}") } const TABLE_HEADER: [&str; 11] = [ diff --git a/src/main.rs b/src/main.rs index 49076661..011639c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -284,6 +284,7 @@ fn make_trace_info( args.max_round_duration, args.max_inflight, args.initial_sequence, + args.icmp_extensions, args.read_timeout, args.packet_size, args.payload_pattern, @@ -302,6 +303,7 @@ fn make_tui_config(args: &TrippyConfig) -> TuiConfig { args.tui_address_mode, args.dns_lookup_as_info, args.tui_as_mode, + args.tui_icmp_extension_mode, args.tui_geoip_mode, args.tui_max_addrs, args.tui_max_samples, @@ -336,6 +338,7 @@ pub struct TraceInfo { pub max_round_duration: Duration, pub max_inflight: u8, pub initial_sequence: u16, + pub icmp_extensions: bool, pub read_timeout: Duration, pub packet_size: u16, pub payload_pattern: u8, @@ -364,6 +367,7 @@ impl TraceInfo { max_round_duration: Duration, max_inflight: u8, initial_sequence: u16, + icmp_extensions: bool, read_timeout: Duration, packet_size: u16, payload_pattern: u8, @@ -388,6 +392,7 @@ impl TraceInfo { max_round_duration, max_inflight, initial_sequence, + icmp_extensions, read_timeout, packet_size, payload_pattern, diff --git a/trippy-config-sample.toml b/trippy-config-sample.toml index e4013525..bb851a17 100644 --- a/trippy-config-sample.toml +++ b/trippy-config-sample.toml @@ -241,6 +241,14 @@ tui-address-mode = "host" # name - Display the AS name tui-as-mode = "asn" +# How to render ICMP extensions +# +# off - Do not show icmp extensions [default] +# mpls - Show MPLS label(s) only +# full - Show full icmp extension data for all known extensions +# all - Show full icmp extension data for all known and unknown classes +tui-icmp-extension-mode = "off" + # How to render GeoIp information. # # Allowed values are: