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.
rustls0.23.40 → 0.23.45 clears RUSTSEC-2026-0285 (TLS 1.3 handshake messages incorrectly accepted across encryption-level boundaries). The advisory reaches ttl transitively viareqwest(PeeringDB) andureq(update check). Also replaces the yankedchacha200.10.0 with 0.10.2.
- dirs 6 → 7 (direct; the only change is Windows
preference_dir, which ttl does not use), hickory-resolver/-proto/-net 0.26.1 → 0.26.3, maxminddb 0.30.0 → 0.30.3, aws-lc-rs 1.16.3 → 1.18.1, aws-lc-sys 0.40.0 → 0.45.0, toml 1.1.4 → 1.1.6.
- The TUI redraws only when something changed. The interactive view previously rebuilt its session snapshot and repainted the terminal on every 16 ms tick (~60 fps) whether or not a probe had landed — at the default 1 s probe interval roughly 55 of every 60 frames were pure churn. Each tick now compares a fingerprint of everything the view renders (hop-stat generation, overlays, theme, status line, replay clock, settings, target list, IX cache state) against the previous frame and skips both the snapshot clone and the draw when they match. Measured on a 9-hop trace at the default interval, 1.4% → 0.4% of one CPU core, and the saving grows with hop count. Output is unchanged and the tick rate is still 16 ms, so input, replay animation, and terminal resizes stay as responsive as before.
- The published crate no longer bundles the demo assets.
docs/*.gifanddocs/*.tapeare excluded from the package, roughly halving the crates.io tarball (1.14 MB → 0.56 MB compressed) for everycargo install ttl. The README still renders the demo from the repository.
rkyv0.8.16 → 0.8.18 clears RUSTSEC-2026-0233 (use-after-free during deserialization) and RUSTSEC-2026-0234 / RUSTSEC-2026-0235 (out-of-bounds reads on crafted archives). The advisory reaches ttl transitively viagetifs→smallvec-wrapper.lru0.18.0 → 0.18.2 clears RUSTSEC-2026-0253 (potential use-after-free from missing panic safety inLruCache::pop()). The advisory reaches ttl transitively viaratatui→ratatui-core.
- maxminddb 0.29 → 0.30 (direct), plus tokio 1.52.3 → 1.53.1, clap 4.6.1 → 4.6.6, clap_complete 4.6.7 → 4.6.9, futures 0.3.32 → 0.3.34, libc 0.2.186 → 0.2.189, serde 1.0.228 → 1.0.229, serde_json 1.0.150 → 1.0.151, socket2 0.6.4 → 0.6.5, and anyhow 1.0.103 → 1.0.104.
- Opt out of the startup update check (#110). The GitHub release check can now be disabled five ways: the
--no-update-checkflag, theDO_NOT_TRACK=1(cross-tool standard) orTTL_NO_UPDATE_CHECK=1environment variables, ano_update_check = truepreference in~/.config/ttl/config.toml, the Update Check toggle in the TUI Settings modal, or a build with--no-default-features, which compiles the check out entirely and drops theupdate-informerdependency. Addresses package-repository, air-gapped, and privacy use cases where an unconditional phone-home is unexpected. Anexamples/config.tomlnow documents the available preferences.
crossbeam-epoch0.9.18 → 0.9.20 clears RUSTSEC-2026-0204 (invalid pointer dereference in thefmt::Pointerimpl forAtomic/Shared). The advisory reaches ttl transitively viamoka→hickory-resolver; the bump keepscargo auditclean.
- Response TTL and return-path estimation on macOS/BSD IPv4 (#117). The hardened control-message parser required a 4-byte
int, but the BSDs and macOS deliver the IPv4IP_RECVTTLvalue as a singleu_char— that 1-byte message was dropped, blanking the reply TTL and the return-hop / path-asymmetry estimate derived from it on those platforms. The value is now read width-aware (4-byteinton Linux and for the IPv6 hop-limit, single byte on BSD/macOS IPv4) while preserving the short-message over-read protection. Exercised on real macOS and FreeBSD kernels in CI. - Panic-proofing across the CLI, probe, and TUI paths. CLI validation rejects out-of-range ports, counts, intervals, and DSCP values and guards
base_port + ttl/src_port + flowoverflow with clean errors instead of panicking (#99); arithmetic on clock jumps and oversized packets uses saturating/checked math (#101); the TUI no longer panics on multi-byte (UTF-8) input or cursor editing (#100, #116). - Unsafe socket code hardened against undefined behavior (#102). Control-message parsing uses unaligned reads and validates
cmsglengths before dereferencing, removing potential misaligned reads and out-of-bounds access on malformed ancillary data. - PMTUD correlation, lock ordering, and enrichment dedup (#104). Corrects a Path MTU Discovery correlation edge case, eliminates an opposing lock-acquisition order in the timeout-cleanup path (deadlock risk), and de-duplicates in-flight enrichment lookups.
- IPv6 correctness. Extension headers inside quoted ICMP error payloads are now parsed so probes correlate correctly (#114, LAN-144), and the interface scope ID is propagated through the receive/bind paths so link-local responders are no longer ambiguous (#113, LAN-143).
- Bounded enrichment caches (#115, LAN-145). The ASN/geo/rDNS/IX response caches are capped at 10,000 entries (expiring by TTL, then evicting oldest) so long-running or high-fan-out traces cannot grow memory without bound.
- Replay robustness (#116, LAN-150/181). Saved-session replay validates its input instead of asserting, and
--replay/--diffare correctly exempt from the live-trace batch validations (e.g.--replay --jsonno longer demands-c). Corrupt session durations deserialize to an error rather than panicking. - File security and output hygiene (#103). Auto-generated export filenames are sanitized (path separators, control characters, and Windows reserved names) to prevent traversal; the preferences file is created and kept at
0600; enrichment cache files are written atomically (temp + rename); report/CSV output is sanitized. - Export filename length (#119). A maximum-length FQDN plus the fixed
ttl-<target>-<timestamp>.jsonoverhead could exceed the 255-byte per-component filesystem limit and fail JSON export withENAMETOOLONG; the target component is now capped on a UTF-8 boundary.
- maxminddb 0.28.1 → 0.29.0, clap_complete 4.6.5 → 4.6.7, anyhow 1.0.102 → 1.0.103.
- CI/release GitHub Actions bumped to current majors:
checkoutv6 → v7,cachev5 → v6, and the docker actionssetup-buildxv3 → v4,setup-qemuv3 → v4,loginv3 → v4,metadatav5 → v6,build-pushv6 → v7.
- Traces no longer collapse to a single hop on macOS and other fast hosts (#12).
sendtois asynchronous on macOS/BSD: the kernel stamps each queued datagram with the socket's current IP TTL at drain time, so a rapidsetsockopt(IP_TTL); sendloop could emit every probe with the final TTL, leaving only the destination/gateway visible — intermittently, and more often on fast Apple Silicon (reproduced on the wire on an M2, macOS 26.5.1). The previous fixed 500µs delay only masked the race.- IPv4 (all platforms): every probe is now sent through a single raw
IP_HDRINCLsocket with the TTL written directly into a hand-built IP header (ICMP/UDP/TCP), removing the race by construction — no per-probe socket churn, no timing delay. The per-OSip_len/ip_offbyte order is handled (host order on macOS/NetBSD, network order on Linux/FreeBSD ≥11) and validated on real Linux, macOS, and FreeBSD kernels in CI. - IPv6: each probe is sent from a fresh socket on the BSD-derived platforms (macOS, FreeBSD, NetBSD) — there is no IPv4-style
IP_HDRINCLfor IPv6 — gated by theper_probe_sendcfg. Linux reuses one shared socket (itssendtonever exhibited the race). - The interim 500µs inter-probe delay has been removed. The rapid probe sweeps are now race-free (IPv4 via
IP_HDRINCL, IPv6 via per-probe sockets on all BSD-derived platforms), so the timing stopgap is no longer needed. The only remaining shared-socket send is the single IPv6 PMTUD probe per round, which is an isolated send (not a back-to-back burst) and so is unaffected. - Correlation is unaffected — the receiver matches on the probe sequence / payload-embedded identifier. IPv4 Path MTU Discovery now also works on NetBSD, since the Don't Fragment bit is set in the hand-built header rather than via the unavailable
IP_DONTFRAGsocket option.
- IPv4 (all platforms): every probe is now sent through a single raw
quinn-proto0.11.14 → 0.11.15, clearing RUSTSEC-2026-0185. The advisory affects a transitive dependency (reqwest's unused HTTP/3 feature) that ttl never compiles into the binary, so it was not reachable at runtime; the bump keepscargo auditclean.- socket2 0.6.3 → 0.6.4, reqwest 0.13.3 → 0.13.4, ratatui 0.30.0 → 0.30.2, chrono 0.4.44 → 0.4.45 (patch updates).
- Interactive target selection: run
ttlwith no arguments to open an empty session, then pressoto add targets — also works mid-session in any live TUI run. Hostnames resolve in the background with a loading state; probe engines (and receivers, for a new IP family) spawn at runtime. Entering an already-traced host switches to it. - Daemon mode (
--daemon): headless probing with no per-hop stdout, for container/monitoring deployments. Combine with--prometheusand/or--stream-jsonto consume the data. - Prometheus exporter (
--prometheus <ADDR>, e.g.:9090): OpenMetrics endpoint with per-hop sent/response/timeout counters, RTT avg/min/max/stddev gauges, loss ratio, ECMP responder count, hop identity info series, and per-target reachability/path-length metrics. IncludesGET /healthzfor container orchestration. Hand-rolled over the existing tokio runtime — no new dependencies. - SIGTERM handling:
docker stopnow triggers the same graceful shutdown as Ctrl-C (unix). - Official Dockerfile: multi-stage Alpine/musl build producing a minimal image;
.dockerignoreincluded. - GHCR container publishing: releases now push multi-arch (amd64/arm64) images to
ghcr.io/lance0/ttl, assembled from the prebuilt static musl release binaries. Newaarch64-unknown-linux-muslrelease artifact (also useful for glibc-free arm64 distros). - Trace diffing (
--diff before.json after.json): compare two saved sessions hop by hop. Reports path changes (primary responder differs), added/lost hops, ECMP responder-set changes, avg RTT deltas (highlighted when ≥5ms and ≥20%), loss changes, and destination reachability.--jsonemits the diff as machine-readable JSON. - Streaming JSON output (
--stream-json): emit each probe event (reply/timeout/late_reply) as one line of JSON on stdout for piping to jq/grep/monitoring pipelines. Event lines match the saved-sessioneventsschema with atargetfield added; a per-targetsummaryline is emitted at end of stream. Implies--no-tui; memory stays bounded on infinite runs.
- IX prefix lookup now uses a binary radix trie instead of a linear scan: O(prefix_len) per responder IP (≤32/128 node hops) instead of O(n) over ~2,000 PeeringDB prefixes. No behavior change.
- maxminddb 0.27.3 → 0.28.1, getifs 0.4.0 → 0.6.1, tokio 1.52.1 → 1.52.3, serde_json 1.0.149 → 1.0.150, clap_complete 4.6.3 → 4.6.5
- Pre-commit hooks (
.pre-commit-config.yaml):cargo fmtandcargo clippy --all-targets -- -D warningson every commit,cargo test --libon every push. Setup documented in CONTRIBUTING.md for both prek (fast Rust port) and the original Pythonpre-commit. - CI:
cargo clippy --all-targets -- -D warningsnow runs on macOS and FreeBSD in addition to Linux. Catches platform-specific cfg-gating regressions before merge. - README: NetBSD pkgsrc installation instructions; replay controls listed in the Keybindings table.
- hickory-resolver upgraded 0.25 → 0.26. Internal API migration in DNS/ASN lookup modules — transparent to users.
- toml upgraded 0.9 → 1.x. No code changes required.
- CI:
softprops/action-gh-releaseupgraded v2 → v3 (Node.js 24 runtime).
- DNS resolver fallback (#71): Restored Google DNS fallback when the system resolver builder is constructable but its
build()step fails. Caught in Copilot review of the hickory 0.26 migration. - macOS clippy warnings (#72): Unused imports and
is_dgramvariable on macOS-only paths, plus threeneedless_returnlints insrc/probe/socket.rs. Contributed by @SSakutaro. - FreeBSD/NetBSD dead-code warnings: DGRAM ICMP socket helpers now cfg-gated to match their call sites.
- clippy 1.95 lints:
collapsible_matchin TUI key handlers andunnecessary_sort_byin IX prefix sorting. - FreeBSD CI: Install
ca_root_nssbefore fetching crates to avoid SSL verification failures from the FreeBSD VM image.
- hickory-proto via hickory-resolver 0.26.1: fixes RUSTSEC-2026-0119 (O(n²) DNS name compression CPU exhaustion). RUSTSEC-2026-0118 (NSEC3 unbounded loop) also no longer applies — ttl does not validate DNSSEC.
- rustls-webpki 0.103.13: cumulative fixes for RUSTSEC-2026-0049, 0098, 0099, 0104.
- aws-lc-sys 0.39.0: fixes RUSTSEC-2026-0044/0045/0046/0047/0048 (CRL distribution scope, AES-CCM timing side-channel, X.509 wildcard bypass, PKCS7 validation bypass).
- quinn-proto 0.11.14: fixes RUSTSEC-2026-0037 (Quinn endpoint DoS — not exploitable in ttl, which only acts as a TLS client).
- libc 0.2.182 → 0.2.186, tokio 1.49 → 1.50, socket2 0.6.2 → 0.6.3, clap 4.5 → 4.6, clap_complete 4.5 → 4.6, proptest 1.10 → 1.11, chrono 0.4.43 → 0.4.44
- 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