HackPy Part 1. – Traceroute

Hi. Welcome to the first post in the series, where I am going to show you the capabilities of Python in terms of building your own tools. I have showed those examples on two meetups, but I feel they are worth sharing here for those of you who did not have an opportunity to see it. You can see other parts under the hackpy tag. The goal of this series is to show you how internals of certain tools are build and to extend your (and mine as well of course) knowledge. You may want to subscribe to newsletter so that you won’t miss any new posts.

Today I am going to show you how traceroute tool is built. To do this, I am going to use scapy library to achieve this.

How does the traceroute actually work?

First, we need to understand what we need to achieve. So, to the point. Traceroute is a tool used for tracking… Traces. Yes, you’ve got it. Traces of IP packets to be specific. Let’s see how it works.

$ sudo traceroute 9.9.9.9
Password:
traceroute to 9.9.9.9 (9.9.9.9), 64 hops max, 52 byte packets
 1  192.168.1.100 (192.168.1.100)  2.112 ms  12.012 ms  10.737 ms
 2  10.31.0.1 (10.31.0.1)  5.716 ms  5.756 ms  5.286 ms
 3  10.100.100.1 (10.100.100.1)  14.880 ms  7.376 ms  3.663 ms
 4  host-185-228-111-240.netronik.pl (185.228.111.240)  5.606 ms  7.336 ms  3.437 ms
 5  host-93-105-90-41.static.warszawa.virtuaoperator.pl (93.105.90.41)  8.182 ms  8.766 ms  7.516 ms
 6  pch.plix.pl (195.182.218.189)  13.663 ms  17.341 ms  12.021 ms
 7  dns9.quad9.net (9.9.9.9)  11.948 ms !Z  11.293 ms !Z  12.638 ms !Z

Consecutive hops are listed in the order of reaching them. One thing that you need to know is that there’s no standard protocol for achieving this goal. We need to hack the existing tools a little bit. Let’s take a closer look at the IP header.

Credits to: https://busy.org/@alexcodes/icmp-and-python

The part that’s interesting to us is the TTL part of IP header. It defines how long (in terms of the number of visited hosts) will the packet live. Whenever a new host on the route to the destination is reached, this number is decremented. When it’s zeroed, the packet is dropped and source host receives information from the host that dropped the packet.

This means that if we set this value to 1, we’ll discover the nearest hop. When we increase it to 2, we’ll discover the host after it and so on.

We are going to use IP combined with ICMP (Internet Control Message Protocol), which is used for diagnosis. You may know it from the ping command used to check connectivity.

Implementation

Our implementation of the traceroute is pretty straightforward:

#!/usr/bin/env python3.8

import sys

from scapy.layers.inet import IP, ICMP
from scapy.sendrecv import sr1


def main(host: str):
    for ttl in range(1, 17):
        packet = IP(dst=host, ttl=ttl)/ICMP()
        if not (response := sr1(packet, verbose=0, timeout=2)):
            print('Did not receive any response. Quitting')
            return
        elif response.src == host:
            print(f'{ttl}\t{response.src}\tdestination reached.')
            return
        else:
            print(f'{ttl}\t{response.src}')
    print('TTL over 16. Quitting.')


if __name__ == '__main__':
    main(sys.argv[1])

In main() function we loop over numbers from 1 to 16 inclusively, which determine the TTL value. Since the TTL is 8-bit value, this can be increased if needed. The packet object is in fact the ICMP/IP packet (we add layers of the network communication using division operator in scapy), where we define the destination and ttl and leave other values as default.

Next we check the response value. Note that I am using Python in newest version – 3.8 which utilizes the assignment in expression operator known as the walrus operator. If you want to run this example using older version of Python, you’ll need to change this line to:

response = sr1(packet, verbose=0, timeout=2)
if response:
    # ...

If there’s no response, we quit, if the response source is the same as requested destination, it means entire route has been discovered, otherwise, we print the visited host and go on with our loop.

Let’s trace

OK, it’s time we tested our code. First, let’s run the traceroute system command.

traceroute 9.9.9.9
traceroute to 9.9.9.9 (9.9.9.9), 64 hops max, 52 byte packets
 1  192.168.1.100 (192.168.1.100)  9.221 ms  1.385 ms  0.982 ms
 2  10.31.0.1 (10.31.0.1)  2.008 ms  1.657 ms  1.585 ms
 3  10.100.100.1 (10.100.100.1)  3.777 ms  3.137 ms  4.517 ms
 4  host-185-228-111-240.netronik.pl (185.228.111.240)  3.960 ms  24.365 ms  6.112 ms
 5  host-93-105-90-41.static.warszawa.virtuaoperator.pl (93.105.90.41)  7.803 ms  22.463 ms  21.799 ms
 6  pch.plix.pl (195.182.218.189)  10.992 ms  11.865 ms  11.601 ms
 7  dns9.quad9.net (9.9.9.9)  30.174 ms !Z  11.903 ms !Z  9.937 ms !Z

And our code for comparison:

sudo ./traceroute.py 9.9.9.9
1	192.168.1.100
2	10.31.0.1
3	10.100.100.1
4	185.228.111.240
5	93.105.90.41
6	195.182.218.189
7	9.9.9.9	destination reached.

Results match, although they don’t have to (routes may change just because). It’s also less informative, but it’s easily extendable in any way we can imagine. I strongly encourage you to play around with it yourself.

Summary

That’s it for now, I’ll show more possibilities in future posts, so stay tuned, subscribe to newsletter, support me on patreon and visit those additional resources:

Read also