mtr-rs: porting mtr to Rust, wire-compatible with the C original
Source: github.com/seitzbg/mtr-rs
mtr is the tool I reach for when a network starts acting up. It folds traceroute and ping into one view that keeps updating and shows every hop on the path. I wanted to understand it by rebuilding it. I also wanted the result to be safe Rust that does not break compatibility with the tool everyone already relies on. So mtr-rs is a port rather than a rewrite of the ideas. The protocol, the probe engine, and the report formats are upstream's. The memory safety and the range-checking are the parts I added.
Two binaries that talk to the originals
Like upstream, mtr-rs is two binaries. There is the unprivileged mtr-rs client and the privileged mtr-rs-packet helper. Both speak the C mtr 0.96 line protocol, so each half also works with its C counterpart. The Rust client can drive the C mtr-packet. The C mtr can drive the Rust helper. The binaries are named mtr-rs and mtr-rs-packet so they install next to your distro's mtr with no conflict.
A sans-IO core
The workspace is four crates and the split is the design:
mtr-protois the codec for the text line protocol between client and helper.mtr-coreis the probing engine: scheduling, per-hop statistics, and ECMP path tracking. It does no I/O. It is a state machine ofCommand,Event, andUserActionthat depends only onmtr-proto.mtr(binarymtr-rs) runs the async event loop on Tokio, plus theratatuiTUI and async DNS and AS-number lookups through hickory.mtr-packet(binarymtr-rs-packet) is the raw-socket helper behind a platform-abstractedProbeBackend.
Because the core does no I/O, the interesting behaviour (scheduling, statistics, ECMP) is testable with a seeded RNG and no sockets involved. The TUI has insta snapshot tests of rendered screens at several terminal sizes. Those cover the paused, prompt, help, and too-small states.
Safe, without changing what mtr does
The proto, core, and packet crates all carry #![forbid(unsafe_code)]. Even the packet helper builds and parses raw ICMP, and it does that socket work through nix and socket2 rather than hand-written unsafe.
I did not want a port that quietly "fixed" mtr into something incompatible. So there is a documented Differences from C mtr section. Every deviation has a code comment pointing at the exact C source it departs from. A few of them:
-jwith several targets prints one JSON array. C concatenates objects into invalid JSON.- CSV output quotes fields that contain commas, quotes, or newlines, following RFC 4180. C never quotes.
- The helper uses the IPv4 IHL field to find headers. C assumes 20 bytes and misparses IP options.
- A negative
-cis a range error. C accepts it, and it clamps a too-small-Uinstead of rejecting it.
These are not bugs I added. They are the spots where C silently wraps or clamps while the Rust version checks the range. The option parser even reproduces strtol's hex, octal, and sign handling and getopt's quirks on purpose. Then it rejects the integer overflows C would let through.
Privileges, dropped early
On Linux the helper opens its raw sockets from a CAP_NET_RAW file capability. Then it drops setuid, setgid, and capabilities before it reads any of stdin. Without that capability it falls back to unprivileged ICMP/UDP datagram sockets, gated by net.ipv4.ping_group_range. macOS and FreeBSD have no such fallback, so there the helper is setuid-root and the drop is setuid(getuid()). The one capability it keeps when granted is CAP_NET_ADMIN. It holds that one because SO_MARK (the -M flag) has to be set per-probe after the drop.
mtr-rs example.org # interactive TUI (q quits, ? for keys)
mtr-rs -rwz -c 5 example.org # wide report with AS numbers
mtr-rs -j example.org | jq . # C mtr's JSON schema (an array for several targets)
mtr-rs --theme dracula example.org # or nord, solarized, gruvboxChecking that the port is faithful
Past the unit and snapshot tests there are cargo-fuzz targets on the protocol parser and the ICMP and MPLS decoders. The check I trust most is that the upstream Python compatibility suites run unmodified against the Rust helper. CI runs all of it as root on Linux, macOS, and FreeBSD. The raw-socket loopback probes actually run on each platform instead of getting skipped where they matter.
Where it is
mtr-rs is at v0.4.3 under GPL-2.0 to match upstream. There is a Homebrew tap, .deb and FreeBSD .pkg packages, and signed and notarized macOS binaries with SHA256SUMS and provenance attestations. It is at github.com/seitzbg/mtr-rs. It came together quickly but the goal was never a traceroute clone. It was something I would trust in place of the C tool that can also stand in for either half of it.