ftr 0.10.0

A fast, parallel ICMP traceroute with ASN lookup, reverse DNS, and ISP detection
Documentation
# ftr Improvement Plan

Plan from a full-project review (code, docs, CI, and a feature-gap analysis
against SwiftFTR 0.13.0) against Rust best practices as of mid-2026. Sections
are stack-ranked — top is next; each is a shippable unit. Delete sections as
they merge (git has the history).

Context: ftr is in maintenance mode (SwiftFTR is the primary macOS client), but
has external users (GitHub stars, crates.io, APT repo, one open issue — #22
requesting IPv6). The v0.8.0 release (PRs #23–#35) covered docs accuracy,
correctness fixes, dependency updates, CI/supply-chain hardening, IPv6
validation spikes, the breaking API cleanup, edition 2024, system DNS
resolvers, and the tests lint-debt payoff; what remains is below.

## Follow-ups from landed work (small, independent)

- DNS: TCP fallback on truncated (TC) responses (`DnsError::Truncated`
  exists; nothing retries over TCP). Windows system-resolver discovery
  (`GetAdaptersAddresses`). Automatic DNS-config change watching to
  complement `dns::refresh_system_dns()` — macOS via `notify(3)` on
  `com.apple.system.SystemConfiguration.dns_configuration` (Chromium's
  approach, <https://issues.chromium.org/issues/40182831>; the c-ares
  maintainers catalog per-platform equivalents in c-ares/c-ares#613),
  Linux via inotify on resolv.conf. `#[non_exhaustive]` on
  `ReverseDnsError`/`DnsRecord` (skipped in #33 to avoid cross-PR
  conflicts).
- Releases: adopt crates.io Trusted Publishing (OIDC) — requires one-time
  configuration on crates.io by the maintainer, then swap the token step in
  `release.yml` for `rust-lang/crates-io-auth-action`. Restructure the
  two-phase draft-release flow so tagging can't leave crates.io/APT silently
  unpublished. Add macOS release artifacts (brew tap currently builds from
  source).
- Coverage: make the llvm-cov job blocking once it proves stable (it kept
  `continue-on-error: true` at introduction).
- criterion is held at 0.7 because 0.8 declares `rust-version = 1.86` — bump
  together with the next MSRV raise.
- Unprivileged macOS traceroute: `macos.rs` only implements raw ICMP
  (root-gated by Darwin), but macOS supports unprivileged `SOCK_DGRAM`
  ICMP — SwiftFTR does full traceroutes without root this way, and the v6
  spike proved DGRAM receives Time Exceeded unprivileged
  (`docs/IPV6_DESIGN.md`). Add a DGRAM fallback like Linux's; falls out
  naturally from IPv6 integration, which needs the DGRAM path anyway.
  Remember Darwin demuxes v4 DGRAM ICMP by identifier (SwiftFTR's 0.8.0
  regression) but does NOT for v6.

## Dependency diet (standing theme + a ladder of PRs)

Maintainer directive (2026-07-17): as an overall theme, reduce the size and
complexity of the dependency tree — SwiftFTR's zero-third-party-deps is the
aspiration. Every new dependency needs justification; prefer hand-rolled
minimal implementations for narrow needs (the 0.7.0 hickory/reqwest/pnet
replacements are the model). Baseline 2026-07-17: **78 transitive crates
from 10 direct** (macOS; Linux adds vendored OpenSSL). Ladder, biggest win
first:

- **Kill ureq + TLS (32 crates, 40% of the tree)**: its sole use is the
  HTTPS public-IP fallback. STUN stays primary (it reflects the probing
  socket's true public mapping). Replace the HTTPS fallback with a STUN
  server (or trivial UDP address-echo) on the Network Weather DigitalOcean
  droplet — infrastructure we control, same protocol, zero TLS. Maintainer
  ruling (2026-07-17): DNS whoami techniques (Akamai/Cloudflare/Google) are
  NOT acceptable — the reported address is the recursive resolver's egress,
  not the client's, whenever queries traverse a forwarding resolver, and
  port-53 interception corrupts even direct queries. (Cloudflare's
  `cdn-cgi/trace` endpoints are useful only if an HTTPS fallback survives.)
  Also removes the vendored-OpenSSL Linux build and the TLS provider
  fragility (a runtime panic lived there until PR #39).
- **getrandom (3 crates)**: only seeds STUN transaction IDs and DNS query
  IDs — `std::hash::RandomState` per-process entropy hashed with a counter
  suffices for these non-crypto IDs (document the security posture: DNS ID
  is anti-spoofing defense-in-depth alongside source-port randomization).
- **ip_network + ip_network_table (3 crates)**: ASN cache longest-prefix
  match over a few hundred prefixes — a sorted-Vec binary search or small
  hand-rolled trie replaces it.
- **serde + serde_json (9 crates incl. proc-macro build deps)**: ftr only
  *serializes* (JSON output); a small hand-rolled JSON emitter with correct
  string escaping covers it. TODO.md already contemplates this.
- **clap (19 crates)**: 16 flags; a minimal parser (or `lexopt`-style
  single-crate) with hand-written --help. Trade-off: loses completions and
  polish — do last, only if the theme still has appetite.
- **thiserror (8 crates, build-time syn/quote)**: mechanical hand-written
  `Display`/`Error` impls. Low value per churn — optional.
- **tokio stays**: the async API is the product; consumers depend on it.

Do NOT batch these with feature work; each rung is its own PR with
before/after `cargo tree` counts in the body.

## Performance & reliability (two PRs)

- Replace 1ms busy-poll receive loops (`linux.rs`, `macos.rs`, `bsd.rs`) with
  tokio `AsyncFd` readiness — the dominant avoidable CPU cost; also fixes
  detached receiver tasks outliving the trace (tie receivers to the engine's
  `JoinSet`), removes `env::var("CI")` reads inside poll loops, and the
  per-packet throwaway struct allocations.
- Consider shared-socket receive demux instead of socket-per-probe. Unique
  ICMP identifier per concurrent socket, validated on Echo Reply AND on the id
  embedded in Time Exceeded/Unreachable payloads (see `docs/IPV6_DESIGN.md`  mandatory for v6 on Darwin, and SwiftFTR's 0.8.0 v4 regression).
- Drop redundant double-locking: outer `tokio::RwLock` around internally
  locked `AsnCache`/`RdnsCache`; make `RdnsCache::get` not take a write lock
  per read.
- Testing hardening: proptest + cargo-fuzz targets for `src/socket/icmp.rs`
  (reject IHL < 5 while at it) and the DNS parser; unit tests for the `unsafe`
  `recvmsg`/CMSG path (linux) and Windows reply-struct reinterpret; gate live-
  network tests behind an env var and serialize them (SwiftFTR's
  `NetworkTestGate` pattern) instead of letting them flake CI.

## IPv6: polish + remaining validation

IPv6 traceroute ships on every supported platform as of 0.10.0 (macOS,
Linux, BSD in 0.9.0 via PRs #38–#43; Windows in 0.10.0 via #50), each
live-validated on real hardware/VMs with external v6. Remaining:

- Deferred live validation (needs privileged access we couldn't script):
  router-originated ICMPv6 Time Exceeded observed first-hand on FreeBSD
  (the FreeBSD VM has external v6, but raw sockets need root and the test
  `ftr` user isn't in `wheel`; run from console or `pw group mod wheel -m
  ftr`, then `ftr -6 2001:4860:4860::8888`). OpenBSD's VM has no external
  v6, so its live multi-hop trace needs a v6-connected host. macOS
  root-mode RAW-vs-DGRAM comparison (`sudo cargo run --release --example
  spike_traceroute6`).
- CI gap that keeps biting: clippy runs only on Ubuntu, so `windows.rs` and
  `bsd*.rs` dead-code/lints surface only when someone compiles those
  targets (three separate cleanups so far). Add windows-target (and ideally
  a BSD-target) `clippy --all-targets` cross-check to CI.
- Polish: NAT64/DNS64 handling review (`AI_V4MAPPED`-style synthesis for
  v4 literals on v6-only networks); first-class zone-scoped (`fe80::%if`)
  targets; hop-level zone display.

## Feature ports from SwiftFTR (optional, by appetite)

Stack-ranked by fit for a traceroute tool with external users:

- **Streaming trace API**`impl Stream<Item = StreamingHop>` emitting hops
   in arrival order with a re-probe phase for rate-limited routers. Best
   library-consumer win; SwiftFTR's `StreamingTrace` is the reference.
- **UDP 5-tuple multipath/ECMP discovery** — ftr on Linux already has
   unprivileged UDP traceroute, so true Dublin/Paris-style flow variation is
   *more* attainable here than it was for SwiftFTR (which shipped ICMP-ID
   variation and documented it under-discovers, deferring UDP to its roadmap).
   A differentiating feature, not just parity.
- **TCP/UDP connectivity probes** — connected-UDP trick (ICMP unreachable
   surfaces as `ECONNREFUSED`, no privileges) and non-blocking TCP connect.
   Small, self-contained.
- **Bufferbloat/RPM testing** — probably out of scope for ftr; it drags in
   HTTP load generation and third-party endpoints. Skip unless a user asks.