Analysis

Reimplementing Traceroute in Rust, From Sockets to Raw Packets

Building traceroute from scratch in Rust forces you past the standard library and into raw sockets, the same low-level territory Van Jacobson occupied at Lawrence Berkeley in 1987.

Sam Ortega6 min read
Published
Listen to this article0:00 min
Share this article:
Reimplementing Traceroute in Rust, From Sockets to Raw Packets
AI-generated illustration
This article contains affiliate links, marked with a blue dot. We may earn a small commission at no extra cost to you.

Rust has a way of turning "I'll just reimplement this classic tool" into a masterclass in systems programming. That's exactly what Viacheslav Biriukov, a software engineer and Site Reliability Engineer affiliated with Cloudflare, delivers at his blog biriukov.dev: a hands-on guide to rebuilding traceroute from raw sockets and raw packets in Rust. Known by his GitHub handle brk0v, Biriukov focuses his technical writing on GNU/Linux, SRE, Golang, Rust, async I/O, and network engineering. The traceroute guide sits squarely at the intersection of all of those disciplines, and it's the kind of low-level exercise that reveals why Rust is increasingly the language serious network engineers reach for.

A Tool With Deep Roots

Traceroute is not young software. Van Jacobson wrote the original implementation in 1987 while working at Lawrence Berkeley National Laboratory in Berkeley, California, acting on a suggestion from Steve Deering. The project attracted several early contributors: Guy Almes, Matt Mathis, C. Philip Wood, Tim Seaver, and Ken Adelman all had a hand in it. Jacobson himself is one of the most consequential figures in Internet history. He is credited with saving the early Internet from congestion collapse through his redesign of TCP/IP congestion control (Jacobson's algorithm) in the late 1980s and early 1990s, co-authored tcpdump, and authored RFC 1144 on TCP/IP header compression. The related foundational diagnostic tool, ping, was written by Mike Muuss. These are not obscure tools; they are the bedrock of how engineers have diagnosed network behavior for nearly four decades. Reimplementing one of them forces you to understand it at the protocol level, not just as a command you type.

How Traceroute Actually Works

The mechanism behind traceroute is elegantly simple. Every IP packet carries a Time-To-Live (TTL) field, a counter that each router along a path decrements by one. When TTL reaches zero, the router discards the packet and sends back an ICMP "Time Exceeded" message to the sender. Traceroute exploits this behavior by sending successive packets with incrementing TTL values, starting at 1. The first packet expires at the first hop, the second at the second, and so on, progressively revealing each router along the route.

The traditional implementation sends UDP packets to destination port 33434. That port number is deliberate: nothing is expected to be listening there. When the packet finally survives all intermediate hops and reaches its destination, the target machine responds with an ICMP "Port Unreachable" message. That "Port Unreachable" response is what signals arrival at the final destination, distinguishing it from the "Time Exceeded" messages returned by intermediate hops. The whole system is a clever abuse of two ICMP error types to reconstruct the topology between you and a remote host.

Where Rust's Standard Library Stops

Here is where Rust puts up a wall: receiving those ICMP replies requires a raw socket. Rust's standard library does not natively support raw sockets. The `std::net` module gives you TCP streams and UDP datagrams, but ICMP operates below that abstraction layer, and accessing it means stepping outside what the standard library provides. This is what makes the traceroute reimplementation a genuinely meaningful systems exercise rather than a straightforward port. You are not translating application logic; you are working at the packet level, constructing and parsing IP headers directly.

The community solution is the `pnet` crate, which provides raw socket access along with utilities for constructing and parsing low-level network packets. Getting a raw socket open at all also requires privilege: programs doing this must run as root or hold the `CAP_NET_RAW` Linux capability. That capability model is worth understanding before you run into confusing permission errors. On most systems you can grant it with `setcap cap_net_raw+eip ./your_binary` rather than running the whole binary as root.

Building the Implementation

The core loop of a Rust traceroute implementation follows the same logic as Jacobson's original:

1. Open a raw socket for sending (typically SOCK_RAW with IPPROTO_UDP) and a separate raw socket for receiving ICMP responses.

2. Construct a UDP packet with the destination port set to 33434 and the IP TTL field set to the current hop count, starting at 1.

3. Send the packet, then listen on the ICMP receive socket with a timeout.

4. Parse the incoming ICMP message: a "Time Exceeded" reply means you've mapped an intermediate hop; a "Port Unreachable" reply means you've reached the destination.

5. Extract the source IP of the ICMP reply to identify the router at that hop, record round-trip time, and increment TTL.

6. Repeat until you receive "Port Unreachable" or hit a maximum TTL threshold.

The `pnet` crate handles the packet construction and parsing, giving you structured access to Ethernet, IP, UDP, and ICMP layers without manually indexing byte arrays. That said, you still need to understand what those headers contain and why fields like the TTL checksum need to be recalculated on each send. This is not a paste-and-run exercise.

Biriukov's Broader Network Engineering Work

The traceroute guide is not a standalone curiosity in Biriukov's output. It is part of a sustained, expert-level exploration of Rust for network engineering. His project `trixter` is described as a high-performance, runtime-tunable TCP chaos proxy built on the async Tokio framework, and `tokio-netem` is a library offering pluggable async I/O adaptors that can simulate latency, bandwidth limits, packet slicing, connection termination, and data corruption. Together these projects reflect a serious research interest: using Rust's ownership model and async runtime not just for correctness and safety, but as practical infrastructure for network fault injection and observability. The traceroute guide is the foundational layer under all of that, grounding the more complex tooling in a deep understanding of how packets actually travel.

Rust's Growing Foothold in Network Tooling

Biriukov is not alone in this territory. Kentik, a network observability company, has cited Rust's async model as the reason it chose the language for its synthetic monitoring agent. The argument is practical: Rust's async I/O allows many concurrent I/O-bound tasks, pings and traceroutes included, to run simultaneously without the resource overhead that would come from thread-per-task models in other languages. That efficiency matters at scale, when you are running synthetic monitoring across hundreds or thousands of paths continuously.

On GitHub, multiple independent Rust traceroute implementations exist under handles like mcpherrinm, OliLay, and ilyagrishkov. A `traceroute` crate is also published on crates.io, the Rust package registry. The fact that so many developers have independently worked through this same exercise is telling: reimplementing traceroute is becoming a rite of passage for Rust engineers who want to prove to themselves that they understand both the language and the network stack underneath their applications. Biriukov's guide is the most thorough treatment of that exercise available, written by someone who builds production-grade network tooling for a living.

Know something we missed? Have a correction or additional information?

Submit a Tip

Never miss a story.
Get Rust Programming updates weekly.

The top stories delivered to your inbox.

Free forever · Unsubscribe anytime

Discussion

More Rust Programming News