Understanding Traceroute

A deep dive into how the traceroute command works by rebuilding it in Rust, focusing on TTL manipulation and ICMP protocols.
Previously, I wrote about setting up a Tailscale exit node and appreciated how traffic gets wired to my home network. I wanted to understand traceroute a bit. I’ve never contemplated how it works, and now feels like as good a time as any to do just that. I mean, now’s the time to rewrite it in Rust.
What does traceroute do?
I’ve just used traceroute to investigate how my query is travelling from my computer to my router and to the internet, finally reaching the end server. At a cursory level, it looks like it’s asking “where is this IP” at each level, and I’m not sure how it does that. Traceroute doesn’t actually ask this “where is this IP.” It uses a TTL trick.
But to understand it, let’s write some code.
- Every IP packet has a TTL (Time To Live) field - a counter that starts at some value (usually 64)
- Every router that forwards the packet decrements TTL by 1.
- When a router decrements TTL to 0, it drops the packet and sends back an ICMP “Time Exceeded” message to the sender.
- That ICMP message contains the router’s IP address.
So if we send packets with TTL=1, the first router replies. TTL=2, the second router replies. And so on, until we reach the destination. That’s traceroute.
The first probe
Let’s start with a single function that sends one UDP packet at a given TTL and listens for the ICMP reply. Why UDP? Because these are throwaway packets designed to die in transit. We don’t need TCP’s handshake or delivery guarantees. We just fire bytes at a port and wait for routers to tell us they dropped them.
Let’s walk through this.
Lines 7-9: We create a regular UDP socket and set its TTL. This is the key trick; we’re deliberately setting a low TTL so the packet dies before reaching the destination.
Lines 12-17: A second socket, this time a raw ICMP socket. This one listens for all ICMP packets arriving at our machine, including the “Time Exceeded” replies from routers that dropped our short-lived UDP packet. We need libc::SOCK_RAW here because socket2 doesn’t expose raw socket types directly, and we need root/sudo to open it.
Lines 20-21: We send 32 bytes of zeros to port 33434 on the target. The content doesn’t matter. Port 33434 is the traditional traceroute port; nothing listens there, so when our packet finally does reach the destination, the target responds with ICMP “Port Unreachable” instead of “Time Exceeded,” which is how we know we’ve arrived.
Lines 24-38: We read from the raw ICMP socket. The reply is a raw IP packet; the first 20 bytes are the IP header, and bytes 12-15 contain the source address of whoever sent the ICMP reply (that’s the router that dropped our packet). We use MaybeUninit because Rust won’t let us read uninitialized memory; the unsafe block is safe here since recv tells us exactly how many bytes it wrote.
Lines 42-55: The main loop. We increment TTL from 1 to 15, printing each hop. When the responding IP matches our target, we’ve reached the destination and break out. This needs sudo to run because of the raw ICMP socket. It works! We can see our Tailscale gateway, home router, ISP, and Google’s network.
Knowing when to stop
Right now we only read the source IP from the IP header. But the ICMP message itself starts at byte 20, and its first byte is the type. If we checked buf[20], we could distinguish between Type 11 (Time Exceeded, meaning a router along the way) and Type 3 (Destination Unreachable, meaning we’ve arrived).
Adding timing and multiple probes
Real traceroute shows round-trip time (RTT) for each probe. The fix is straightforward: Instant::now() before send, elapsed() after recv.
Traceroute also sends three probes at each TTL for three reasons:
- Variance: Network latency fluctuates. Three measurements give a feel for consistency.
- Reliability: If one probe times out, we still see the hop.
- Load balancer detection: If different probes hit different IPs at the same TTL, we know there is a load balancer.
Comparing with real traceroute
| Feature | Real traceroute | Ours | |---|---|---| | TTL incrementing | Yes | Yes | | ICMP type checking | Yes | Yes | | Timing (RTT) | Yes | Yes | | 3 probes per hop | Yes | Yes | | DNS reverse lookup | Yes | No | | Port incrementing | Yes | No | | TCP/ICMP modes | Yes | No |
Source: Hacker News












