Adventures in homemade Traceroute — Part I

msingh
9 min readAug 4, 2024

--

Have you ever wondered how data travels from your computer to a server on the internet? What path does it take, and how many stops or ‘hops’ does it make along the way? To answer these questions, we can use a powerful and commonly available tool called ‘traceroute’. This program is found on almost all operating systems and fairly simple to use . In this blog, we will take a look at the traceroute mechanism but more importantly try to code such a program in golang

Here is a sample trace:

$ traceroute -q 1 google.com
traceroute: Warning: google.com has multiple addresses; using 142.251.175.100
traceroute to google.com (142.251.175.100), 64 hops max, 40 byte packets
1 un (192.168.18.1) 4.574 ms
2 1.128.88.116.starhub.net.sg (116.88.128.1) 5.159 ms
3 183.90.44.189 (183.90.44.189) 6.020 ms
4 203.118.6.233 (203.118.6.233) 6.241 ms
5 ..<some other Ips>
13 142.251.247.203 (142.251.247.203) 8.469 ms
14 *
.....<more * for other hops>
23 *
24 sh-in-f100.1e100.net (142.251.175.100) 9.811 ms

As you can see, the output shows the path that data takes when connecting from your computer to the destination (in this case, Google.com). Each line represents a ‘hop’ in the journey, showing the IP address of each router along the way and the time it took to reach that point. Turns out that underlying mechanism of traceroute is actually quite simple

The TTL

The magic of tracing relies on a special header called TTL (Time To Live which realistically should be called the hop count) in the data being sent.

Let’s break down how data travels across the internet to understand this better:

Imagine you’re sending a search query to Google.com. Your data goes through several layers of wrapping, you can think of it as a set of nested envelopes :

  1. Data Layer: Your search query is the innermost ‘letter’. Lets call it E1
  2. TCP Layer: This creates an envelope (E2) around your data, adding info like sequence numbers and data size.
  3. IP Layer: Another envelope (E3) wraps around E2, adding source and destination IP addresses, and our magic header — TTL.
  4. Ethernet Layer: The final envelope (E4) includes hardware-specific info like MAC addresses.

When this fully wrapped packet reaches the first router:

  1. The router opens E4 to read E3’s headers.
  2. It checks the destination IP to decide where to send the packet next.
  3. It decrements the TTL by 1.
  4. If TTL reaches 0, the packet is dropped, and the router sends a control message back to your computer.
  5. This control message includes the router’s IP address — Aha ! the key information for tracing.

Here is a visual that depicts how a packet travels and TTLs being decremented at each hop

Packet dropped at Router4 which responds with ICMP Time Exceeded reply

NOTE: The diagram also shows that there are generally multiple paths from src to destination and each attempt may take different paths

By now, you might see how this helps in tracing the path. If not, don’t worry — we’ll explore this further in the next section.

Traceroute exploits the TTL mechanism to map the path of packets across the internet. Here’s how it operates:

  1. Traceroute sends a series of packets to the destination, starting with a TTL of 1.
  2. The first router receives the packet, decrements TTL to 0, discards it, and sends back an ICMP “Time Exceeded” message.
  3. Traceroute records this router’s IP address and the time taken.
  4. It then sends another packet with TTL set to 2. This reaches the second router before being discarded.
  5. This process repeats, incrementing the TTL each time, until:
  • The packet reaches the destination, or
  • The maximum number of hops is reached (usually 30).

For each “hop,” traceroute typically sends three packets to account for variations in routing.

The program displays each hop’s IP address and the round-trip time for each packet.

ICMP Packets

For this section, please follow along using my homegrown Go program.

See help doc on how to run the trace

We mentioned that ‘Traceroute sends a series of packets,’ but what kind of packets are these, and what does the router send back?

The default behavior of traceroute is to use UDP datagrams but it can have issues with Network Administrators blocking such traffic e.g. Web Servers which don’t really need to allow UDP traffic .

Instead we will use ICMP (Internet Control Message Protocol) which is a network layer protocol used by network devices to diagnose network communication issues. Here’s a bit more detail:

  • ICMP Echo Request: This is the packet our program sends out . When you initiate a traceroute, it sends these packets with incrementing TTL values. We just go builtin types, create and ICMP Echo Message and send it across the wire using a IPv4PacketConn. Note that , IPv4PacketConn will take care of the IP Header creation, we just need to marshal the ICMP Message.
conn.IPv4PacketConn().SetTTL(ttl)

icmpMsg := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: os.Getpid() & 0xffff,
Seq: 1,
Data: []byte("PING.."),
},
}

msg, err := icmpMsg.Marshal(nil)
if err != nil {
return nil, err
}

if _, err = conn.WriteTo(msg, addr); err != nil {
return nil, fmt.Errorf("failed to send ICMP message: %v", err)

}
fmt.Print("Sent ICMP probe to ", addr, " with TTL ", ttl, " ")
  • ICMP Time Exceeded: This is the packet that routers send back when they receive a packet with a TTL that has reached zero. It contains the IP address of the router, which traceroute uses to map the network path as well as the original ICMP Echo Request
  • The function which does the bulk of the processing is
func readICMPResponse(conn *icmp.PacketConn, echoRequest *icmp.Echo)

Few things to note about this function

  • Attempting to read the response will either give us a ICMP reply from Router or it will time out in case Router never sends us anything
  • We decipher the incoming response packet using the excellent Google’s gopacket library . Its not doing anything special here, just converting bytes to a structure so that we can understand the type of the response
reply := make([]byte, 1500)
n, peer, err := conn.ReadFrom(reply)
if err != nil {
debugPrint(`failed to receive ICMP reply:`, err)
return nil, fmt.Errorf("failed to receive ICMP reply")

}

packet := gopacket.NewPacket(reply[:n], layers.LayerTypeICMPv4, gopacket.Default)
icmpLayer := packet.Layer(layers.LayerTypeICMPv4)
if icmpLayer == nil {
return nil, fmt.Errorf("failed to parse ICMP reply")

}
icmpPacket, _ := icmpLayer.(*layers.ICMPv4)
debugPrint("Reply from : ", peer)
  • ICMP Time Exceeded Reply — This is our bread and butter. But we want to be sure that when we get a time exceeded response it is in response to the Echo request we sent out, and we do that here
case layers.ICMPv4TypeTimeExceeded:
fmt.Println("Time exceeded from peer ", peer)

if len(icmpPacket.Payload) >= 28 { // 20 bytes IP header + 8 bytes original ICMP header
// Get the IP header length
ipHeaderLength := int(icmpPacket.Payload[0]&0x0f) * 4
debugPrint("IP Header Length: ", ipHeaderLength)

originalICMP := icmpPacket.Payload[ipHeaderLength:] // Skip the IP header
// 8 is the type for Echo Request
if originalICMP[0] == 8 {
/*
Network byte order is, by convention, big-endian. RFC 1700
*/
code := originalICMP[1] //should be 0
checksum := binary.BigEndian.Uint16(originalICMP[2:4])
id := binary.BigEndian.Uint16(originalICMP[4:6])
seq := binary.BigEndian.Uint16(originalICMP[6:8])
debugPrint(fmt.Sprintf("Data read from ICMP Response => Code: %d, Checksum: %d, ID: %d, Sequence: %d", code, checksum, id, seq))
if id == uint16(echoRequest.ID) && seq == uint16(echoRequest.Seq) {
debugPrint(fmt.Sprintf("Found original message in Time Exceeded payload, ID %d and Sequence %d match\n ", id, seq))
} else {
fmt.Println("IGNORE: Time Exceeded payload does not match original message")
}

} else {
fmt.Println("Original message was not an Echo Request")
}
} else {
fmt.Println("Time Exceeded payload too short to extract original message")
}

The key here is that ICMP Time Exceeded Response, also contains 8 bytes of the original ICMP Echo Request, which if you recall from the code earlier had two fields — ID and Sequence number. We can now check that the repsonse does indeed match the ID and Sequence sent earlier to confirm that indeed this is the case.

There are nuances like the first N (generally 20) bytes of the response contain the original IP header (which we discard) and then read the next 8 bytes. The first of these 8 bytes has to be the type for Echo Request (since that was what we had sent) and the rest 7 bytes are Code, checksum, ID and Sequence which we extract .

Here is how the ICMP Time Exceeded Response format looks like. We are interested in the last 64 bits (8 bytes) to match this against the original request

 RFC 792
Time Exceeded Message

0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Code | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unused |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Internet Header + 64 bits of Original Data Datagram |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

If you like to read more about ICMP, check out RFC 792

  • ICMP Echo Reply: This is the packet sent by the final destination when it receives the ICMP Echo Request. It signifies that the traceroute has reached its target. We double check the ID and Seq here too.
case layers.ICMPv4TypeEchoReply:
fmt.Println("Echo reply from peer ", peer)
// For Echo Reply, ID and Seq are directly available in the ICMP header
if icmpPacket.Id == uint16(echoRequest.ID) && icmpPacket.Seq == uint16(echoRequest.Seq) {
debugPrint("Found original message in Echo Reply, ID and Sequence match\n")
} else {
fmt.Println("IGNORE: Echo Reply does not match original message")
}

Running my go traceroute program, you will get something like below

$ sudo go run tracert.go -proto icmp google.com
Resolved IP address: 142.251.175.102
Sent ICMP probe to 142.251.175.102 with TTL 1 Time exceeded from peer 192.168.18.1
Sent ICMP probe to 142.251.175.102 with TTL 2 Time exceeded from peer 116.88.128.1
Sent ICMP probe to 142.251.175.102 with TTL 3 Time exceeded from peer 183.90.44.189
Sent ICMP probe to 142.251.175.102 with TTL 4 Time exceeded from peer 203.118.6.233
Sent ICMP probe to 142.251.175.102 with TTL 5 Time exceeded from peer 203.118.6.149
Sent ICMP probe to 142.251.175.102 with TTL 6 Time exceeded from peer 203.116.3.102
Sent ICMP probe to 142.251.175.102 with TTL 7 Time exceeded from peer 142.250.166.50
Sent ICMP probe to 142.251.175.102 with TTL 8 Time exceeded from peer 142.250.238.115
Sent ICMP probe to 142.251.175.102 with TTL 9 Time exceeded from peer 192.178.109.94
Sent ICMP probe to 142.251.175.102 with TTL 10 Time exceeded from peer 142.251.230.135
Sent ICMP probe to 142.251.175.102 with TTL 11 Time exceeded from peer 142.251.231.204
Sent ICMP probe to 142.251.175.102 with TTL 12 Time exceeded from peer 209.85.250.189
Sent ICMP probe to 142.251.175.102 with TTL 13 Failed to receive ICMP reply
Sent ICMP probe to 142.251.175.102 with TTL 14 Failed to receive ICMP reply
Sent ICMP probe to 142.251.175.102 with TTL 15 Failed to receive ICMP reply
Sent ICMP probe to 142.251.175.102 with TTL 16 Failed to receive ICMP reply
Sent ICMP probe to 142.251.175.102 with TTL 17 Failed to receive ICMP reply
Sent ICMP probe to 142.251.175.102 with TTL 18 Failed to receive ICMP reply
Sent ICMP probe to 142.251.175.102 with TTL 19 Failed to receive ICMP reply
Sent ICMP probe to 142.251.175.102 with TTL 20 Failed to receive ICMP reply
Sent ICMP probe to 142.251.175.102 with TTL 21 Failed to receive ICMP reply
Sent ICMP probe to 142.251.175.102 with TTL 22 Echo reply from peer 142.251.175.102
Done.

As you can see -

  • Most routers responded with “Time Exceeded” ICMP packet with their IP
  • Many chose not to reply — This is common as these routers may be configured not to respond to such requests, may be busy or may be limiting such requests
  • And finally we saw the Echo response from Destination

Conclusion

The IPs we see when running our Go code are similar to those we get with the standard traceroute utility. Note that there can be multiple paths between your computer and the destination, so the IPs may sometimes differ. However, if you run the program several times, you’ll observe similar results.

Coming up next

This brings us to the end of the first part of our traceroute adventure. In the next part, we’ll try to improve our program to include more information about the routes, similar to what traceroute program does (time taken, Autonomous systems etc.) . I am also experimenting with writing a TCP based traceroute to see how that works (traceroute also supports that). Stay tuned !

--

--

msingh
msingh

No responses yet