Back to Home

Traceroute on Rust: how route tracing works

The article breaks down the traceroute implementation on Rust. It shows how TTL and ICMP messages build a route map. Code is provided with explanations of raw sockets operation and network packet processing.

How traceroute builds a route: technical breakdown on Rust
Advertisement 728x90

# How Traceroute Works: Implementation in Rust from Scratch

Traceroute is a network diagnostics tool that reveals the path packets take through the internet. It relies on two key mechanisms: TTL (Time to Live) in IP headers and ICMP error messages. By incrementing TTL at each step, routers send notifications when the packet's lifetime expires, building a route map. Implementing our own version in Rust in just 80 lines of code shows how low-level network operations become clear with a deep dive into protocols.

UDP Probe Architecture

The critical element of traceroute is sending targeted packets with limited TTL. UDP is used instead of TCP for three reasons:

  • No handshake reduces overhead
  • No delivery guarantee—packets are meant to be dropped
  • High port numbers (starting from 33434) minimize conflicts with running services

The code initializes two sockets:

Google AdInline article slot
let send_sock = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
send_sock.set_ttl_v4(ttl)?;

let recv_sock = Socket::new(
    Domain::IPV4,
    Type::from(libc::SOCK_RAW),
    Some(Protocol::ICMPV4),
)?;

The first socket sends doomed packets with the specified TTL, the second intercepts ICMP responses. Important: raw sockets require root privileges, as they operate at the network stack level.

Parsing ICMP Responses

When a router drops a packet at TTL=0, it sends an ICMP message of type 11 (Time Exceeded). The data structure contains:

  • First 20 bytes—IP header of the response
  • Bytes 12-15—router's IP address
  • Byte 20—ICMP message type

The initial implementation only parsed the IP address:

Google AdInline article slot
if buf.len() >= 20 {
    let ip = Ipv4Addr::new(buf[12], buf[13], buf[14], buf[15]);
    Ok(Some(ip))
}

This led to errors in detecting the endpoint. Proper handling requires checking the ICMP type:

match buf[20] {
    11 => Ok(ProbeResult::Hop(ip)),
    3 if ip == target => Ok(ProbeResult::Reached(ip)),
    3 => Ok(ProbeResult::Hop(ip)),
    _ => Ok(ProbeResult::Timeout),
}

Type 3 (Destination Unreachable) indicates reaching the target only if the IP matches.

Traceroute Optimization

The original traceroute uses two techniques missing from the basic implementation:

Google AdInline article slot
  • Incrementing port number—each subsequent packet is sent to port +1. This allows unambiguous matching of ICMP responses to requests via the Identification field in the UDP header.
  • TCP mode support—when UDP is blocked by firewalls, a SYN packet with low TTL is used. The hop detection mechanism remains the same.

A critical error in early code versions—ignoring the IP address check when receiving Type 3. Without the if ip == target condition, tracing stopped at the first router returning Destination Unreachable.

What’s Important

  • TTL as a control tool—gradually increasing the value allows sequential discovery of each hop
  • ICMP messages—the basis of diagnostics—Time Exceeded (Type 11) and Destination Unreachable (Type 3) form the route map
  • Raw sockets require privileges—low-level network operations are impossible without root rights
  • UDP vs TCP—protocol choice affects firewall evasion, but not the basic algorithm
  • Parsing binary data—direct access to packet bytes requires knowledge of IP/ICMP header structures

Implementing the Stop Condition

A key improvement—correctly detecting when the target is reached. The ProbeResult enum replaces the simple Option<Ipv4Addr>:

enum ProbeResult {
    Hop(Ipv4Addr),
    Reached(Ipv4Addr),
    Timeout,
}

In the main loop, result handling looks like this:

match hop {
    ProbeResult::Hop(ip) => println!("{:>2}  {}", ttl, ip),
    ProbeResult::Reached(ip) => {
        println!("{:>2}  {}", ttl, ip);
        break;
    }
    ProbeResult::Timeout => println!("{:>2}  *", ttl),
}

This ensures the trace stops upon reaching the target host, not on timeout. In real-world scenarios, also add:

  • Three probes per TTL for latency statistics
  • Handling of fragmented packets
  • IPv6 support
  • Distance-based timeouts

Summary and Limitations

The built implementation demonstrates traceroute's core, but has limitations:

  • Doesn't account for asymmetric routes
  • Doesn't handle ICMP Rate Limiting
  • Ignores MPLS tags in modern networks
  • Requires superuser privileges

For production solutions, use libraries like pnet_packet to avoid manual byte parsing. However, writing a raw version is the best way to understand network fundamentals. Every network engineer should implement traceroute at least once to grasp how data travels through the internet.

— Editorial Team

Advertisement 728x90

Read Next