All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Interactive replay controls: Seek (Left/Right ±500ms, [/] ±5s), speed adjust (+/- ±0.5x, 0.5x–5.0x range), Home/End jump to start/end
- Replay progress bar: Shows play state, event count, timeline position, and speed multiplier
- Replay help section: Help overlay (
?) now shows replay-specific keybindings when in replay mode
- Replay timing precision: Switched from f32 to f64 for elapsed time calculations to prevent drift on long replays
- Replay seek safety: Saturating arithmetic prevents overflow on relative seek;
Instant::checked_subprevents underflow on corrupted replay files
- NetBSD UDP probes: Auto-detect source IP for UDP DGRAM sockets on NetBSD. Fixes "No route to host" (EHOSTUNREACH) when sending UDP probes without
--source-ip(#47) - NetBSD IPv4 PMTUD retry spam: PMTUD now terminates immediately on first DF flag failure instead of retrying every probe round. Prints a single warning when
IP_DONTFRAGis unavailable (#47)
- NetBSD platform support (experimental): Raw sockets, IPv6 PMTUD, BSD minimum inter-probe delay. IPv4 PMTUD unavailable (no
IP_DONTFRAG). Interface binding (-i) not supported. (#47)
- Update checker reliability: Changed from blocking
recv_timeout(1s)to non-blockingtry_recv()polling in TUI event loop. Update notifications no longer silently drop when GitHub API response exceeds 1 second - Update checker first-run: Set
update-informerinterval toDuration::ZEROso first launch performs an immediate network check instead of writing a cache file and waiting for the next run
- Updated clap (4.5.58→4.5.60), libc (0.2.181→0.2.182), futures (0.3.31→0.3.32), maxminddb (0.27.1→0.27.3), toml_parser (1.0.7→1.0.9), anyhow (1.0.101→1.0.102), plus transitive deps
- ECMP classification: Detects per-flow vs per-packet ECMP using primary concentration heuristics. Paths column now accurately reflects observed responder count for per-packet load balancing (#46)
Eindicator: New ECMP indicator in main table replaces misleading!(route flap) when ECMP is the actual cause- Effective flow capability: Runtime detection of protocol flow support.
--flows > 1with ICMP now warns and collapses to single-flow instead of silently doing nothing - Receiver flow attribution hardening: Out-of-range source ports (NAT/CGNAT) no longer force-attributed to flow 0. Unknown-flow responses only match pending probes when unambiguous
- Hop detail view: Per-packet ECMP section shows responder count and path count; per-flow ECMP section unchanged
- Last RTT semantics documented:
Lastcolumn tracks primary responder's most recent RTT (code docs + KNOWN_ISSUES) - Main table layout guard tests: Header/cell/width count parity verified across Auto/Compact/Wide x single-flow/multi-flow
- IPv6 RAW payload fallback tests: Echo Reply and Time Exceeded parsing tests for IPv6
- Flap detection suppressed during ECMP:
!indicator no longer fires when per-packet ECMP is detected at a hop - Probe engines use per-session config: Each target's engine sees the effective flow count, not the raw CLI value
- Last RTT column: Main table now shows the most recent probe RTT between Sent and Avg, matching mtr's default column order (#17)
- JAvg and JMax columns: Jitter average and jitter max columns appear in Wide display mode (press
w) (#17) - CSV columns: Added
last_ms,jitter_avg_ms,jitter_max_msto CSV export
- CSV schema (breaking): Header now has 14 columns (was 11).
last_msinserted beforeavg_ms;jitter_avg_msandjitter_max_msappended - ASN column width: Increased auto mode cap (30→40 chars) and wide mode width (24→36 chars) to show more of the AS name
- Updated clap (4.5.54→4.5.58), clap_complete (4.5.65→4.5.66), anyhow (1.0.100→1.0.101), libc (0.2.180→0.2.181), proptest (1.9.0→1.10.0), plus transitive deps
- Hang on exit: Bounded IPv6 echo-reply drain loop to prevent starvation when socket is continuously readable (#41)
- Ctrl+C during shutdown: Ctrl+C now force-exits if cleanup stalls — first press cancels, second press terminates immediately (#41)
- Shutdown tracing: Set
TTL_SHUTDOWN_TRACE=1to emit[shutdown]stage markers for diagnosing exit hangs
- Immediate sent counting: Sent counters now increment when probes leave, not when responses arrive. Matches mtr behavior for real-time feedback (#17)
- FreeBSD ICMP sockets: FreeBSD doesn't support
SOCK_DGRAM + IPPROTO_ICMP, now uses RAW sockets directly. FixesProtocol not supportederror on FreeBSD (#14) - Dual-stack
--resolve-all:--resolve-allwithout-4/-6now traces both IPv4 and IPv6 addresses by spawning dual receivers. Previously silently dropped one address family (#11) - IPv6 Echo Reply double-counting: Fixed sent counter being incremented twice for Linux IPv6 ICMP probes (once at send, once at Echo Reply)
- Dead
create_probe_interval()function
- ASN column format: Now shows
AS##### nameinstead of just the name, so ASN number is always visible even when truncated (#17)
- Sent count consistency: All pending probes are now counted as timeouts when trace completes, ensuring consistent sent counts across all hops (#37)
--jumboflag: Enable 9216-byte max for PMTUD in jumbo frame environments. Without this flag, PMTUD uses standard 1500-byte ethernet max (#28)
- PMTUD accuracy: Standard ethernet (1500 MTU) now reports exact MTU instead of ~1495 (#28)
- TUI lock contention: Snapshot session data before rendering so no locks are held during draw, fixing UI freezes during rapid target switching (#19)
- Stat column desync: Sent counter now updates atomically with Avg/Min/Max/Loss instead of racing ahead (#17)
- Jumbo frame support:
--sizenow accepts up to 9216 bytes (was 1500) for jumbo frame environments. PMTUD binary search also starts at 9216, discovering jumbo MTUs automatically (#28) - Animated replay mode:
--replay <file> --animatereplays saved sessions showing probe-by-probe discovery instead of final state (#9) - Replay speed control:
--speedflag controls replay speed (default 10x, use 1.0 for real-time) - Probe event recording: Sessions now record per-probe events with full correlation info (TTL, seq, flow_id, responder, RTT)
- Replay controls: Press Space to pause/resume animated replay
- Late reply tracking: Responses arriving after timeout are recorded as
late_replyevents for replay accuracy
- FreeBSD build failure: Fix
cargo install ttlon FreeBSD by makinggetifsdependency conditional. Gateway detection unavailable on FreeBSD (uses macOS-specific APIs). - Replay timing accuracy: Events now replay at their recorded timestamps instead of fixed intervals
- Replay pause/resume: Resuming no longer causes time jump or skipped events
- Event timestamp monotonicity: Use monotonic clock (
Instant) for event offsets to prevent clock jump issues
- TUI refresh rate: Increased from 10fps to 60fps for more responsive per-probe updates (#17)
- CI: Added FreeBSD 14.2 build testing via vmactions/freebsd-vm
- In-app update notification: When an update is available, a yellow status bar and banner show the new version. Press 'u' to dismiss.
- Update command in help: Help overlay now shows the install-method-aware update command (e.g.,
brew upgrade ttlorcargo install ttl)
- Update notification TLS: Fix update checker silently failing due to missing TLS support in
ureqbackend
- FreeBSD support (experimental): Basic traceroute now works on FreeBSD 13/14. Requires
sudo. Interface binding (-i) is not supported due to missing kernel APIs.
- Resolver behavior: Target resolution now follows OS resolver order (respects
/etc/gai.confon Linux). Use-4or-6to force a specific IP family. (PR #24 by @n-thumann) - Target list number keys: Pressing 1-9 now selects and closes the dialog in one action
- Target list lockup: Fix lock contention when selecting target from list overlay (#19)
- Export lock contention: Clone session data before file I/O to avoid blocking receiver thread
- Hop navigation lock contention: Extract hop count in scoped block before updating UI state
- Hop detail modal truncation: Modal now dynamically sizes to content, preventing help text cutoff
- Status bar allocation: Use
Cow<str>to avoid string allocation on every frame
- Update notifications: Checks GitHub releases for new versions (daily, cached). Shows install-method-aware update command on exit when a new version is available. Only displays on TTY.
- Gateway detection via kernel APIs: Replaced subprocess-based route detection with direct kernel API calls (netlink on Linux, sysctl on macOS). Instant startup even on DFZ routers with millions of routes (#16)
- DFZ router startup hang: Gateway detection no longer shells out to
ip routewhich could hang on systems with large routing tables (#16)
- Hop detail navigation: Use Up/Down/k/j to navigate between hops while detail view is open, 1-9 to jump directly (#18)
- Version in help: Help overlay now shows version number in title (#22)
- PMTUD accuracy: Trust ICMP-reported MTU directly per RFC 1191, fixing 6-byte underreporting (#23)
- UI lockup with multiple targets: Target list overlay no longer blocks receiver thread (#19)
- Startup hang on routers with large routing tables: Route detection commands now timeout after 2 seconds (#16)
- PMTUD probes inflating hop statistics: PMTUD probes no longer count toward hop sent/received stats (#21)
- macOS: Increase inter-probe delay from 200µs to 500µs to eliminate remaining TTL batching issues (#12)
- Fix clippy warning with Rust 1.93+
- Multi-IP resolution (
--resolve-all): Trace all resolved IP addresses for hostnames- Round-robin DNS (multiple A records) now traceable with single command
- Dual-stack hosts (A + AAAA) supported - compares IPv4 vs IPv6 paths
- Automatic deduplication by IP with hostname aliasing
- Prefers IPv4 by default, falls back to IPv6 if no IPv4 addresses
- Skip warnings show how many addresses were filtered (e.g., "3 IPv6 skipped")
- Title bar shows
hostname -> IPformat when tracing resolved hostnames
- Target list overlay (
lkey): Overview of all resolved targets in multi-target mode- Shows IP address, hostname, hop count, and loss percentage for each target
- Navigate with Up/Down or j/k, select with Enter, jump with 1-9
- Only available when multiple targets are being traced
- Settings modal (
skey): Configure theme, display mode, and PeeringDB API key- Live preview of theme changes
- Display mode selector (auto/compact/wide) for column widths
- PeeringDB API key input with text editing support
- Cache status display (prefix count, age, expiry indicator)
- Press
rin PeeringDB section to refresh cache - Settings persist to config file on exit
- PeeringDB API key persistence: API key saved to
~/.config/ttl/config.toml- Environment variable
PEERINGDB_API_KEYstill takes precedence over saved key
- Environment variable
- Wide mode CLI flag (
--wide): Start with wide (generous column widths) mode - Autosize columns (
wkey): Intelligent column width control- Auto mode (default): Columns auto-fit to longest hostname/ASN content
- Compact mode: Minimal column widths (host: 20, ASN: 12)
- Wide mode: Generous column widths (host: 45, ASN: 24)
- Press
wto cycle: auto → compact → wide → auto - Long hostnames like
po-300-xar01.alexandria.va.bad.comcast.netdisplay fully in auto mode - Maximum caps prevent layout blowout (host: 60 chars, ASN: 30 chars)
- Display mode persisted to
~/.config/ttl/config.toml
- Status bar shows
l listhint when multiple targets are available - Status bar shows
w displayhint for cycling display modes - Help overlay updated with
s(Settings),l(Target list), andw(Display mode) keybindings - Settings modal now shows "Display Mode" selector instead of "Wide Mode" toggle
- Intel Mac binaries restored: Pre-built x86_64-apple-darwin binaries available again
- TTL batching bug (#12) was likely the cause of previous Intel Mac issues
- Uses new
macos-15-intelGitHub Actions runner
- macOS: Probes sent with wrong TTL in initial burst (#12)
- Rapid
setsockopt(IP_TTL)calls were batched by macOS kernel - First N probes sent with same TTL, causing only 1 hop to display
- Added 200µs minimum delay between probes on macOS to ensure TTL changes take effect
- Workaround:
--rateflag also fixes this by adding delay between probes
- Rapid
- Fix cargo fmt in integration tests (no functional changes from 0.12.7)
- Non-responding hops frozen after destination found: Hops showing
* * *now continue to be probed after destination is discovered, matching mtr/trippy behavior- Previously,
Sentcounters froze on non-responding hops after completion - Now probes all TTLs up to destination every round
- Allows detecting hops that recover from rate limiting
- Previously,
- Ctrl+C to quit TUI: Now handles Ctrl+C (and ETX) in addition to
q [max_ttl=30]warning: Title bar shows warning when destination not found and using default max_ttl of 30; hints to try--max-ttl 64for long paths
- More probes sent after destination found (all TTLs probed each round)
- Bounded by
dest_ttl, notmax_ttl - Matches mtr/trippy probing behavior
- Bounded by
- ICMPv6 checksum computation: Fix IPv6 traceroute not detecting destinations
- ICMPv6 packets had checksum 0, relying on kernel to fill it in (it didn't)
- Destinations dropped packets with invalid checksums; intermediate hops worked
- Added manual ICMPv6 checksum computation with RFC 8200 pseudo-header
- Algorithm derived from trippy (BSD-licensed) with known-value test verification
- Socket now bound to source IP for IPv6 to ensure checksum consistency
- IPv6 address display: Increased width for full IPv6 addresses
- TUI host column: 28 → 42 chars for IPv6 (prevents truncation)
- Text report host column: 40 → 46 chars
- IPv6 ICMP traceroute: Fix 100% packet loss on Linux for destination hop
- Linux delivers ICMPv6 Echo Reply only to the socket that sent the request
- Added send socket polling for Echo Reply in IPv6 ICMP mode (Linux-only)
- Intermediate hops (Time Exceeded) were unaffected; only destination detection was broken
- ICMPv6 Echo Request now uses correct type 128 (was incorrectly using type 8)
- Hop detail dialog: Add
Enterandqkeys to close dialog (PR #6 by @themoog)Enternow toggles the dialog (open and close)qprovides familiar quit-key for TUI users- Improves accessibility for users with non-functional Escape keys
- Cargo.lock: Now tracked in version control for reproducible builds
- Best practice for binary applications per Cargo documentation
- Enables deterministic builds for package managers (nixpkgs, etc.)
- Linux binary compatibility: Switch x86_64 builds to musl libc
- Pre-built binaries now work on Debian 11/12 and other older distros
- Previously required glibc 2.39 (Ubuntu 24.04+), now fully static
- Hop detail view stats: Fixed "Sent: 0" display bug in hop detail panel
- Hop detail now correctly shows hop-level sent/received/loss stats
- Previously showed per-responder
sent(always 0) instead of hop-levelsent - Note: Per-responder sent can't be tracked (we don't know which responder will reply before sending)
- Quick Start documentation: Made Linux
setcapcommand more prominent- Shows how to run without sudo on Linux after one-time capability setup
- Clarifies macOS always requires sudo
- Terminal injection protection: Sanitize DNS hostnames, ASN names, and IX info before display
- Filters control characters from external data sources (PTR records, Team Cymru, PeeringDB)
- Prevents malicious terminal escape sequences from affecting the TUI
- --count semantics:
-c Nnow sends N probe rounds (one probe per TTL), not N × max_ttl probes- Each round sends probes to all active TTLs in a single interval
- Behavior now matches user expectations:
-c 10= 10 rounds of probing - Updated help text to clarify "probe rounds" semantics
- Port overflow validation:
--src-port+--flowscombination now correctly validated- Fixed off-by-one: ports 65520 + 16 flows (max port 65535) now accepted
- Clear error message shows the computed maximum port number
- Sequence wrap prevention: Reject
--timeout> 256 ×--interval- ProbeId uses u8 sequence (0-255), wraps every 256 intervals
- Validation prevents mis-correlation when old probes outlive sequence wrap
- Dead code removal: Removed unused
recv_icmp_for_udpfunction
- Dependencies updated:
- hickory-resolver 0.24 → 0.25
- socket2 0.5 → 0.6
- reqwest 0.12 → 0.13
- dirs 5.0 → 6.0
- toml 0.8 → 0.9
- ipnetwork 0.20 → 0.21
- Removed unused
thiserrordependency
- Added
sanitize_display()helper in lookup module for control character filtering - Added 5 CLI validation tests for port overflow and timeout/interval checks
- All three probe modes (ICMP, UDP, TCP) now track
rounds_completedfor consistent-cbehavior
- Shell completions: Generate completions for bash, zsh, fish, and powershell via
--completions <shell> - WSL2 documentation: Added Windows via WSL2 installation guide to README
- Document
PEERINGDB_API_KEYenvironment variable for higher API rate limits - Extract
RECENT_WINDOW_SIZEconstant for cleaner code - Better documentation for rate limit detection edge cases
- Linux permission error: Fail fast with clear instructions (setcap or sudo) instead of silently falling back to broken unprivileged mode
- Multi-target response misattribution: Fix bug where responses could be attributed to wrong target when tracing multiple destinations concurrently
- Extract original destination IP from quoted ICMP error packets for direct lookup
- Use responder IP for Echo Reply disambiguation (responder IS the target)
- Eliminates ambiguous linear target iteration
- MSG_CTRUNC detection: Return
NoneTTL when control message is truncated to prevent unreliable asymmetry detection - IPv6 permission check on Linux: Warn if IPv6 sockets unavailable (mirrors macOS behavior)
- macOS CI: Add macOS test job to catch platform-specific issues before release
- Remove panic-able
unwrap()from MPLS label parsing (use direct array conversion)
- macOS Sequoia (15) support: Document as "build from source" only
- Pre-built binaries are built on Tahoe (26) and may have display issues on Sequoia
- Users on macOS 15 should use
cargo install ttlto compile from source - Updated README Platform Support table to clarify compatibility
- Switch macOS build to
macos-latestrunner (Tahoe 26)- Did not resolve Sequoia compatibility (see 0.11.3)
- macOS traceroute 100% packet loss: Fix ICMP traceroute showing all hops as
* * *- DGRAM ICMP sockets cannot receive ICMP Time Exceeded messages from intermediate routers
- Now uses RAW socket for receiving (can receive all ICMP types) while keeping DGRAM for sending (supports IP_TTL)
- Added payload-based correlation fallback for RAW receive paths (fixes 100% loss when macOS kernel modifies ICMP identifier)
- Requires
sudoon macOS since RAW sockets need root privileges - Clear error message when run without elevated privileges
- Linux unprivileged ICMP: Restore support for unprivileged ICMP sockets (broken in v0.11.0)
- Linux users with
ping_group_rangeenabled can run without sudo - Falls back to DGRAM sockets when RAW sockets are unavailable
- Linux users with
- IPv6 DGRAM availability check: Warn on macOS if IPv6 DGRAM sockets are unavailable
- macOS traceroute: Fix ICMP traceroute showing only 1 hop on macOS
- Use
SOCK_DGRAMinstead ofSOCK_RAWfor ICMP sockets on macOS - macOS raw sockets don't support
IP_TTLsetsockopt, preventing TTL manipulation - DGRAM sockets allow setting TTL per-packet for proper traceroute functionality
- Added DGRAM-aware packet parsing (no IP header in received packets)
- Embedded ProbeId in ICMP payload for correlation fallback (macOS may override identifier)
- Use
- Platform support: Drop Intel Mac (x86_64-apple-darwin) binaries - Apple Silicon only
- Intel Macs can still build from source via
cargo install ttl
- Intel Macs can still build from source via
- Cross-compilation: Switch from native-tls to rustls-tls to avoid OpenSSL dependency for aarch64 builds
- macOS build: Fix
msg_controllentype mismatch (u32 vs usize) - Deprecation warning: Use
bind_device_by_index_v4instead of deprecatedbind_device_by_index
- CLI examples in help:
--helpnow shows usage examples and detection indicator legend - Smoke test script:
tests/smoke.shfor cross-platform verification
- README improvements: Homebrew install, simplified permissions, Known Limitations section, better troubleshooting
Highlights: Path MTU discovery, ICMP rate limit detection, route flap detection, asymmetric routing detection, TTL manipulation detection, and CI/CD automation. Major release for network diagnostic capabilities.
- Path MTU discovery (
--pmtud): Binary search to find maximum unfragmented packet size- Uses DF (Don't Fragment) flag to detect MTU limits
- Binary search algorithm: starts at 1500, converges to within 8 bytes
- Shows progress in TUI title bar:
[MTU: min-max]during search,[MTU: X]when complete - Extracts MTU from ICMP Fragmentation Needed (IPv4 Type 3 Code 4) and ICMPv6 Packet Too Big (Type 2)
- Handles EMSGSIZE errors for local interface MTU limits
- Requires 2 consecutive successes or failures before moving binary search bounds (handles network flakiness)
- IPv4 minimum: 68 bytes (RFC 791), IPv6 minimum: 1280 bytes (RFC 8200)
- Conflicts with
--size(mutually exclusive)
- Packet size control (
--size): Set probe packet size for MTU testing- Range: 36-1500 bytes for IPv4, 56-1500 bytes for IPv6
- Total packet size includes IP header (20/40 bytes) + protocol header + payload
- Packets sent with DF (Don't Fragment) flag for proper MTU discovery
- Works with all probe protocols (ICMP, UDP, TCP)
- DSCP/ToS marking (
--dscp): Set IP header DSCP field (0-63) for QoS policy testing- DSCP 46 = Expedited Forwarding (EF) for VoIP traffic
- DSCP 34 = AF41 for video traffic
- Useful for testing QoS policies and seeing where traffic gets remarked
- Works with all probe protocols (ICMP, UDP, TCP)
- Supports both IPv4 (TOS) and IPv6 (Traffic Class)
- GitHub Actions CI: Automated build, test, clippy, and format checks on PRs
- Runs on ubuntu-latest for all pushes to master and PRs
- Strict clippy (
-D warnings) catches issues before merge
- Binary releases: Automated builds on version tags via GitHub Actions
- Linux x86_64 and aarch64 (cross-compiled)
- macOS x86_64 (Intel) and aarch64 (Apple Silicon)
- Pre-built binaries attached to GitHub releases
- SHA256 checksums included for verification
- cargo-audit security check before release
- Rate limiting (
--rate): Limit probes per second to avoid triggering router rate limits- Useful for slow links or avoiding overwhelming targets
--rate 0= unlimited (default),--rate 10= 10 probes/sec max- Global limit applies across all flows
- Source IP selection (
--source-ip): Force probes to use a specific source IP address- Useful for multi-homed hosts with multiple IPs
- Works with all probe protocols (ICMP, UDP, TCP)
- Validates source IP family matches target family
- ICMP rate limit detection: Identify when routers are rate-limiting ICMP responses
- Detects misleading packet loss caused by router rate limiting (not actual packet drops)
- Three detection heuristics:
- Isolated hop loss: Loss at hop N but 0% loss downstream = rate limiting
- Uniform flow loss: All flows losing equally in Paris/Dublin mode = hop-level limiting
- Stable loss ratio: Consistent loss percentage over time = rate limiting (vs fluctuating congestion)
- Loss% column shows "RL" suffix (e.g., "50%RL") when rate limiting suspected
- Title bar shows
[RL?]indicator when any hop has rate limiting detected - Hop detail view shows detection reason, confidence level, and mitigation tip
- Tip suggests slower probing with
-i 1.0or-i 2.0to avoid triggering limits - Detection automatically clears when loss drops below threshold
- First-hop gateway detection: Display source IP and default gateway in TUI
- Shows routing info in title bar:
eth0 (192.168.1.100 → 192.168.1.1) - Auto-detects default gateway from system routing table
- Works with or without
--interfaceflag - Parses
ip route showon Linux,route -n get defaulton macOS - Gateway info also populated when using
--interfaceoption
- Shows routing info in title bar:
- Route flap detection: Detect when primary responder IP changes at a hop
- Indicates routing instability in single-flow mode
- Main table shows "!" after hostname when flaps detected
- Hop detail view shows route change history (last 5 changes)
- Uses sticky tie-breaker with margin (requires new IP to exceed old by 2+ responses)
- Minimum 5 responses before recording flaps (avoids startup noise)
- Disabled in multi-flow mode (
--flows > 1) where path changes are expected - History capped at 50 changes per hop
- Asymmetric routing detection: Detect when return path differs from forward path
- Extracts response TTL from ICMP packets using
recvmsg()withIP_RECVTTL/IPV6_RECVHOPLIMIT - Estimates return hops using common initial TTL defaults (64, 128, 255)
- Compares forward TTL vs estimated return hops to detect asymmetry
- Flags asymmetry when difference >= 3 hops in >50% of samples (minimum 5 samples)
- Title bar shows
[ASYM]indicator when any hop has asymmetric routing detected - Main table shows "~" after hostname when asymmetry suspected at that hop
- Hop detail view shows routing symmetry section: forward hops, return hops, confidence
- High variance in return hops suggests return-path ECMP
- Disabled in multi-flow mode (like route flap detection)
- Extracts response TTL from ICMP packets using
- TTL manipulation detection: Detect middleboxes that modify IP TTL values
- Analyzes quoted TTL in ICMP Time Exceeded (code 0) responses only
- Code 0 = TTL exceeded in transit; code 1 = fragment reassembly exceeded (ignored)
- Per RFC 1812, quoted TTL should be 0 or 1 (post-decrement or pre-decrement)
- Detects: transparent proxies (quoted TTL == sent TTL), abnormal quoted TTL > 1
- Hop 1 guard: avoids false positive when sent_ttl=1 and quoted_ttl=1 (normal pre-decrement)
- Title bar shows
[TTL!]indicator when manipulation detected - Main table shows "^" after hostname at affected hops
- Hop detail view shows: sent TTL, last quoted TTL, normal/anomalous sample counts
- Works in both single-flow and multi-flow modes (unlike asymmetry/flap detection)
- Hysteresis clearing resets anomaly counters to prevent re-triggering
- PeeringDB pagination: Added
limit=0to API requests to fetch all IX records- Without this, only the first page of results was cached, missing many IX detections
- PeeringDB User-Agent: Added proper User-Agent header to avoid 403 Forbidden responses
- PeeringDB API key support: Set
PEERINGDB_API_KEYenv var for higher rate limits- Anonymous API access is rate-limited (1/hour for large queries)
- API key authentication provides 40 requests/minute
- IX lookup race condition: Use
OnceCell::get_or_try_initfor thread-safe lazy loading- Previously, concurrent lookups could trigger multiple parallel API fetches
get_or_try_initonly fills cell on success, allowing retries after backoff on failure
- IX lookup failure backoff: Skip retries for 5 minutes after load failure
- Prevents log spam and repeated API hits on unstable networks
- Longest prefix match: Sort prefixes by length descending for correct matching
- Previously returned first match; now returns most specific (longest) prefix
- Rate limit reset:
reset_statsnow clears rate limit detection state- Previously RL warnings could persist after reset or replay
- Stable loss ratio calculation: Fixed segment length calculation for non-divisible window sizes
- Previously third segment used wrong divisor, skewing detection
- Rate limit clearing hysteresis: Require 2 consecutive negative checks before clearing
- Also clears when downstream loss rises above 10% (isolated loss no longer applies)
- Force clears after 5 negatives regardless (signal gone if heuristics stop matching)
- Prevents UI flicker while ensuring stale RL doesn't linger
- Stable-loss uses recent window: Detection now uses recent_results loss, not lifetime
- Fixes sticky RL during recovery when lifetime loss is still high but recent is 0%
- PMTUD probe ID collision: Added
is_pmtudflag to pending map key- Completely eliminates collision between normal and PMTUD probes with same ProbeId
- PMTUD consecutive counter logic: Direction changes now reset opposite counter
- Ensures 2 truly consecutive results before advancing binary search bounds
- PMTUD response size verification: Only process responses matching current probe size
- Ignores late responses from previous probe sizes that could corrupt state
- IPv6 Packet Too Big handling: Added dedicated
PacketTooBigenum variant- ICMPv6 Type 2 now correctly triggers PMTUD MTU clamping
- Multi-target JSON output: Multiple targets now wrapped in JSON array
- Previously output invalid JSON (concatenated objects without delimiters)
- TUI pause state sync: Switching targets now syncs pause indicator with target's state
- Previously pause indicator could be stale after Tab/n target switch
- Dependencies updated: ratatui 0.28→0.30, crossterm 0.28→0.29, maxminddb 0.24→0.27
- Fixes RUSTSEC-2025-0132 (maxminddb unsafe memmap), RUSTSEC-2024-0436 (paste unmaintained)
- Security audit CI: Added
.github/workflows/audit.ymlfor daily RustSec advisory checks
- PMTUD:
PmtudStatestruct with binary search state (min/max bounds, success/failure counters) - PMTUD:
PmtudPhaseenum (WaitingForDestination, Searching, Complete) - PMTUD:
set_dont_fragment()insocket.rsfor Linux (IP_MTU_DISCOVER) and macOS (IP_DONTFRAG) - PMTUD: MTU extraction from ICMP errors in
correlate.rs(Type 3 Code 4 for IPv4, Type 2 for ICMPv6) - PMTUD:
packet_sizefield inPendingProbefor correlation - PMTUD: Engine sends PMTUD probes at destination TTL after normal traceroute finds destination
- New
src/state/ratelimit.rsmodule for detection logic RateLimitInfostruct with suspected flag, confidence (0-1), reason, and loss data- Background async worker runs analysis every 2 seconds (lightweight)
- Detection integrates with all modes: interactive TUI, batch, and streaming
- JSON export includes rate limit data via serde
- IX lookup uses
tokio::sync::OnceCellfor thread-safe lazy initialization - Refactored
Receiver::new()andspawn_receiver()to useReceiverConfigstruct (9 args → 4 args) - Renamed internal
fixed_portfield toport_fixedfor Rust naming consistency - Gateway detection:
detect_gateway_ipv4()anddetect_gateway_ipv6()ininterface.rs - Gateway detection:
detect_default_gateway()for auto-detected interface routing InterfaceInfoextended withgateway_ipv4andgateway_ipv6fieldsSessionextended withsource_ipandgatewayfields for TUI display
- IX detection via PeeringDB: Identify Internet Exchange points in the path
- Fetches IX peering LAN prefixes from PeeringDB API
- Matches hop IPs against IX prefixes (IPv4 and IPv6)
- Shows IX name, city, and country in hop detail view
- Data cached locally for 24 hours to respect API rate limits
- Cache stored in
~/.cache/ttl/peeringdb/ix_cache.json - Disable with
--no-ixflag
- New
src/lookup/ix.rsmodule for PeeringDB integration IxInfostruct added toResponderStatsfor IX dataIxLookuphandles API fetching, caching, and prefix matching- Background
run_ix_workerupdates session state like ASN/GeoIP workers - Added
reqwestdependency for HTTP requests
- Interface binding: Force probes through a specific network interface
- New
--interface <NAME>flag binds all sockets to the specified interface - Useful for multi-homed hosts, VPN split tunneling, or deterministic egress path selection
- Works with all probe protocols (ICMP, UDP, TCP)
- Interface name shown in TUI title bar ("via eth0") and report output
- Linux uses
SO_BINDTODEVICE, macOS usesIP_BOUND_IF
- New
- Asymmetric routing support: New
--recv-anyflag- Requires
--interfaceto be set - Disables receiver socket binding to interface
- Allows receiving replies on any interface (for asymmetric routing, VPN scenarios)
- Send sockets remain bound to the specified interface
- Requires
- IPv6 interface detection: Fixed bug where global IPv6 addresses were incorrectly rejected
- The link-local check used bitwise NOT (
!v6.segments()[0]) instead of comparison (!=) - Global IPv6 addresses like
2001:db8::1now correctly detected on dual-stack interfaces
- The link-local check used bitwise NOT (
- Link-local only rejection: Non-loopback interfaces with only link-local IPv6 now return clear error
- Link-local addresses require scope IDs and can't reach Internet targets
- Error message explains the issue and suggests assigning a global address
- Auto-protocol UDP binding: Auto-protocol mode now tests UDP with interface binding
- Previously could select UDP even if interface binding would fail later
- Now fails fast with clear error instead of confusing runtime failure
- New
src/probe/interface.rsmodule for cross-platform interface validation and binding is_link_local_ipv6()helper function shared between production code and testsInterfaceInfostruct holds validated interface name, index, IPv4/IPv6 addresses- Interface passed through
ProbeEngine,Receiver, and all socket creation functions recv_anyfield inConfigcontrols receiver binding behavior- Uses
pnet::datalink::interfaces()for enumeration,socket2for binding
- Enrichment in batch/streaming modes: DNS, ASN, and GeoIP lookups now work in
--json,--report,--csv, and--no-tuimodes- Previously enrichment workers only spawned in interactive TUI mode
- Batch mode waits for enrichment to settle before export
- Streaming mode shows hostnames progressively as DNS resolves
- Terminal state restoration: TUI now properly restores terminal on early errors or panics
- Added
scopeguard::defer!guard to ensure cleanup runs on all exit paths - Prevents terminal being left in raw/alternate screen mode on crash
- Added
- Added
scopeguard = "1"dependency for cleanup guards run_batch_mode()andrun_streaming_mode()now spawn enrichment workers- Streaming output includes hostname column when resolved
- Multiple simultaneous targets: Trace to multiple destinations at once
- Pass multiple targets:
ttl 8.8.8.8 1.1.1.1 google.com - Tab/n to switch to next target, Shift-Tab/N for previous
- Target indicator in title bar shows
[1/3]for current target - Per-target pause/reset (p/r affect only current target)
- Each target runs its own probe engine with independent state
- Pass multiple targets:
- SessionMap architecture: Shared sessions map for multi-target support
SessionMap = Arc<RwLock<HashMap<IpAddr, Arc<RwLock<Session>>>>>- Single receiver demultiplexes responses to correct session
- Lookup workers (DNS, ASN, GeoIP) iterate all sessions
PendingKeynow includes target IP:(ProbeId, flow_id, IpAddr)- Receiver iterates target list to find matching probe
run_tui()accepts SessionMap and targets listMainView::with_target_info()for target indicator display- Mixed IPv4/IPv6 targets not supported (single receiver limitation)
- NAT detection: Detect when NAT devices rewrite source ports
- Compare sent source port vs returned port in ICMP error payloads
- NAT indicator column ("!") in TUI when multi-flow mode enabled
[NAT]warning in title bar when NAT detected anywhere- Per-hop NAT details in hop detail view (match/rewrite counts, samples)
- Warning when NAT may affect ECMP accuracy
NatInfostruct tracks port matches and rewrites per hop
PendingProbenow storesoriginal_src_portfor NAT detectionHop::record_nat_check()compares original vs returned source portsSession::has_nat()checks if NAT detected at any hop- NAT info included in JSON export via serde
- Paris/Dublin traceroute (ECMP detection): Multi-flow probing to discover parallel network paths
- New
--flows Nflag: Send probes on N different flows (1-16, default 1) - New
--src-port BASEflag: Base source port for flow identification (default 50000) - Each flow uses a different source port (UDP/TCP) for path differentiation
- Routers using ECMP load balancing will route different flows to different paths
- New
- Per-flow path tracking: Track which responders are seen on each flow
FlowPathStatsstruct tracks sent/received/responder per flowHop::has_ecmp()detects when multiple paths existHop::ecmp_paths()returns list of (flow_id, responder) pairsHop::path_count()returns number of unique paths discovered
- ECMP display in TUI:
- New "Paths" column in main table when
--flows > 1 - Column shows number of unique responders across flows
- Highlighted in warning color when ECMP detected (>1 path)
- Hop detail view shows per-flow path breakdown with hostnames
- New "Paths" column in main table when
- Source port extraction: ICMP error parsing extracts original source port for flow correlation
- Loss percentage "pulsing": Fixed visual glitch where loss would pulse on each hop
- Loss now calculated from completed probes only:
timeouts / (received + timeouts) - In-flight probes no longer count as temporary losses
- Added
timeoutscounter toHopstruct for accurate tracking
- Loss now calculated from completed probes only:
- Multi-flow UDP probing: Creates separate bound sockets per flow
- Multi-flow TCP probing: Varies source port in raw SYN packets
- Flow ID tracked in
PendingProbefor response correlation ParsedResponse.src_portfield for flow identification from ICMP errorsPendingMapkeyed by(ProbeId, flow_id)to prevent multi-flow entry collisions- Flow derivation validates port range to avoid mis-attribution from NAT rewrites
- Backward compatible:
--flows 1(default) = identical to previous behavior
- NAT devices may rewrite source ports, causing multi-flow correlation to fail (responses will appear as losses)
- ASN column in main table: Network provider/ISP now visible at a glance
- Shows AS name (e.g., "GOOGLE", "COMCAST") for each hop
- Complements existing ASN details in hop detail view
- TCP SYN probing mode: Send TCP SYN packets instead of ICMP Echo
- Enable with
-p tcpor--protocol tcp - Default port 80, customizable with
--portflag - Probe ID encoded in TCP sequence number for correlation
- Proper TCP checksum calculation with pseudo-header
- Enable with
- Protocol auto-detection: Automatically select best available protocol
- New default mode (
-p auto): tries ICMP → UDP → TCP in order - Falls back when socket creation fails (e.g., no raw socket permission)
- Seamless degradation for unprivileged users
- New default mode (
- Fixed port option: Disable per-TTL port variation for UDP/TCP
- New
--fixed-portflag keeps destination port constant - Useful for probing specific services (e.g., DNS on port 53)
- New
- High-rate optimizations: Improved performance at fast probe intervals
- Batch drain limit (100 packets) prevents receiver starvation
- Batched state updates reduce lock contention
- Single lock acquisition per batch instead of per-packet
- Receiver error tracking: Stop after 50 consecutive socket errors
- Prevents infinite error loops when socket fails persistently
- Logs error count progress (e.g., "Receive error (5/50): ...")
- Graceful shutdown with descriptive error message
- ASN lookup: Automatic ASN enrichment via Team Cymru DNS (enabled by default)
- Displays ASN number, name, and BGP prefix in hop detail view
- Supports both IPv4 and IPv6 addresses
- Caching for 1 hour to reduce DNS queries
- Disable with
--no-asnflag
- GeoIP lookup: Optional geolocation via MaxMind GeoLite2 database
- Displays city, region, country, and coordinates in hop detail view
- Auto-discovers database in common paths (~/.local/share/ttl/, /usr/share/GeoIP/)
- Specify custom path with
--geoip-dbflag - Disable with
--no-geoflag
- UDP probing mode: Send UDP probes instead of ICMP Echo
- Enable with
-p udpor--protocol udp - Uses classic traceroute port range (33434+)
- Port can be customized with
--portflag - Probe ID encoded in UDP payload for correlation
- Enable with
- Receiver panic handler: Captures panic details instead of generic error message
- Uses
catch_unwindfor clean error reporting - Improves debugging when receiver thread fails
- Uses
- Enhanced jitter statistics: avg_jitter, max_jitter, and last_rtt now tracked and displayed
- RTT percentiles: p50, p95, p99 calculated from sample history (last 256 samples)
- MPLS label parsing: RFC 4884/4950 ICMP extensions parsed for MPLS label stacks
- Enhanced hop detail view: Now displays percentiles, enhanced jitter stats, last RTT, and MPLS labels
- Parallel DNS resolution: Up to 10 concurrent reverse DNS lookups for faster hostname resolution
- Startup false drops: Fixed race condition where fast ICMP responses arrived before probe was registered
- Shared pending map with insert-before-send eliminates registration race
- Socket drain before timeout cleanup prevents dropping queued responses
- Improved accuracy for low-latency first hops
- ASN TXT parsing: Fixed handling of quoted/split TXT records from Team Cymru DNS
- Jitter semantics: Clarified that jitter measures RTT variance, not inter-packet timing
- Added detailed code comments explaining RFC 3550-inspired EWMA calculation
- New "Statistics Explained" section in README with jitter/metrics documentation
- TCP probe module (
src/probe/tcp.rs) with SYN packet building and checksum calculation - TCP checksum uses actual source IP via UDP connect routing lookup (not 0.0.0.0)
- TCP correlation support in ICMP error payload parsing
- Batched receiver state updates for reduced lock contention
- Added
futurescrate for parallel async operations - Sample history stored in circular buffer (256 entries) for percentile calculations
- MplsLabel struct with RFC 4950 format parsing
- MPLS extension parsing uses RFC 4884 length field (not fixed 128-byte offset)
- Clarified jitter UI labels to distinguish smoothed vs raw sample stats
- ASN lookup uses Team Cymru DNS (origin.asn.cymru.com, AS name lookup)
- GeoIP lookup uses MaxMind GeoLite2-City database format
- UDP probe correlation extracts ProbeId from UDP payload in ICMP errors
- Receiver error tracking with consecutive failure counting
- Library API boundary cleanup: Internal modules now use
pub(crate)visibility- Public API:
config,export,statemodules - Internal (crate-only):
cli,lookup,probe,trace,tuimodules - Binary still has full access to all modules
- Public API:
- Theme persistence: saves selected theme to
~/.config/ttl/config.toml - Theme automatically restored on next launch
- CLI
--themeflag still overrides saved preference
- Theme support with 11 built-in themes via
--themeflag - Themes: default, kawaii, cyber, dracula, monochrome, matrix, nord, gruvbox, catppuccin, tokyo_night, solarized
- Runtime theme cycling with
tkey in TUI - Theme-aware UI rendering (borders, status colors, highlights)
- Initial release
- ICMP Echo probing with TTL sweep (1-30 by default)
- IPv4 and IPv6 support with extension header handling
- Real-time TUI built with ratatui
- Hop statistics: loss%, min/avg/max RTT, standard deviation, jitter
- ECMP detection showing multiple responders per TTL
- Reverse DNS resolution for hop IPs
- Export formats: JSON, CSV, text report
- Session replay from saved JSON files
- Interactive TUI with j/k navigation, hop detail view
- Loss-aware sparkline visualization
- Pause/resume probing (p key)
- Stats reset (r key)
- Destination detection (automatically stops at actual hop count)
- Platform support documentation (Linux, macOS)
- Welford's online algorithm for numerically stable mean/variance
- RFC 3550-style smoothed jitter calculation (measures RTT variance)
- Probe correlation via ICMP sequence field encoding
- IPv6 extension header parsing (Hop-by-Hop, Routing, Destination Options)
- ICMP checksum validation for IPv4 Echo Reply
- Graceful handling of receive buffer size limits
- Max TTL validation (capped at 64 to prevent resource exhaustion)
- Replay file size limit (10MB max to prevent DoS)
- Troubleshooting section in README (permissions, high loss, IPv6, DNS)
- 92 unit tests covering ICMP parsing, stats calculation, session state
- 20 integration tests for probe→state pipeline
- 9 property-based tests (proptest) for packet parsing robustness
- Tests for IPv6 extension headers, ECMP scenarios, edge cases