> ## Content Index
> Fetch the complete content index at: https://blog.bsd-unix.net/llms.txt
> Use this file to discover other available public pages before exploring further.

# mtr-rs: porting mtr to Rust, wire-compatible with the C original
- URL: https://blog.bsd-unix.net/mtr-rs-porting-mtr-to-rust-wire-compatible-with-the-c-original/
- Published: 2026-09-10T16:36:09.000Z
- Updated: 2026-09-10T16:52:10.000Z
- Description: A faithful Rust port of mtr (My TraceRoute): the same two-binary split, the same 0.96 line protocol so the halves interoperate with C mtr, a sans-IO core, and forbid(unsafe_code) over the packet crate.
- Author: Bryan Seitz
- Tags: Projects, Networking, Rust

**Source:** [github.com/seitzbg/mtr-rs](https://github.com/seitzbg/mtr-rs?ref=blog.bsd-unix.net)

`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-proto` is the codec for the text line protocol between client and helper.
- `mtr-core` is the probing engine: scheduling, per-hop statistics, and ECMP path tracking. It does no I/O. It is a state machine of `Command`, `Event`, and `UserAction` that depends only on `mtr-proto`.
- `mtr` (binary `mtr-rs`) runs the async event loop on Tokio, plus the `ratatui` TUI and async DNS and AS-number lookups through hickory.
- `mtr-packet` (binary `mtr-rs-packet`) is the raw-socket helper behind a platform-abstracted `ProbeBackend`.

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:

- `-j` with 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 `-c` is a range error. C accepts it, and it clamps a too-small `-U` instead 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.

```bash
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, gruvbox
```

## Checking 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](https://github.com/seitzbg/mtr-rs?ref=blog.bsd-unix.net). 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.