Traceroute to the Front Door: Trimming Public Net Hops

Hop_0:

Traceroute is a wonderful computer networking diagnostic tool. This article will attempt a traceroute script written in Python, including a customization that can identify just the routers within your private network (from the host up to and including the public/internet gateway).

Prerequisites

Python 2
comes with mac OS and most Linux distributions,
here(https://www.python.org/downloads/windows/) is a link for installing it on Windows

Scapy
once Python 2 is installed: as root in a terminal, issue the command:

pip install scapy

Traceroute Computer Networking Fundamentals

The fundamentals we will deal with are 1) The idea of private versus public networks, 2) Routing, and 3) Packets.

Packets contain two types of information: information related to helping routers direct the packet and other information that usually serves other purposes.

Routing is a complex set of standards which govern how data is transferred between computers on a network.

Public/Private (network) is a distinction made which identifies a divide between a service provider and a private institution (a divide between an end-user developing their internal network versus the ISP designing the infrastructure they operate).

In this Python script, we build packets to manipulate routing standards to give us leading information — with which we will be able to deduce the line between the public and private side of our private network.

Hop_1:

The code

# Author: Alexander DeForge
# File: PrivatePathToPublicGateway.py
# Platform: Ubuntu 16.04.3 LTS
# Implementation: Python 2.7.12
# Date: 08/22/2017
# Purpose: Identify the path to the Net public gateway through a private network
# Use (as root and tested for the given Platform and Implementation):
#
#    python PrivatePathToPublicGateway.py http://httpbin.org/ip
#
#    Scapy interactive environment could not accept root access; therefore, this
#      script must be run outside of the Scapy interactive environment.
#    Python interactive environment could not import Scapy; therefore, this script
#      must be run outside of the Python interactive environment.
#    This script has been successfully run from a shell environment with root
#      privileges

from scapy.all import *
import urllib2
import sys
import socket
import json

conf.verb=0

def contains(target, total):
    for x in total:
        if target == x:
            return True
    return False

def publicIP(verbose, getIdentityAddr):
    if verbose:
        print "Identifying public IP of gateway..."
    try:
        if verbose:
            print "NO ACCESS TO INTERNET!"
        # correct getIdentityAddr (send via cmdline): http://httpbin.org/ip
        my_ip = json.load(urllib2.urlopen(getIdentityAddr))['origin']
        if verbose:
            print "...success."
        return my_ip
    except urllib2.URLError:
        if verbose:
            print "...failure."
        return None

class route:
    def __init__(self):
        self.path = []
    def add(self, e):
        self.path.append(e)
    def getPath(self):
        return self.path

def routeTracer(dest, verbose, nextHop=1):
    MAX_RETRY_COUNT = 5
    TIMEOUT = 1
    tryCount = 0
    Route = route()

    while (True):
        # scapy
        ip = IP(ttl=nextHop, dst=dest)
        res = sr1(ip / TCP(), timeout=TIMEOUT)

        # there was no response, but the retry threshold hasn't been reached
        if res is None and tryCount < MAX_RETRY_COUNT:
            if verbose:
                print "Retrying for the {0} time on Hop number {1}".format(tryCount+1, nextHop)
            tryCount += 1
            continue
        # there was no response, but the retry threshold has been reached/exceeded
        elif res is None and tryCount >= MAX_RETRY_COUNT:
            if verbose:
                print "TIMEOUT: EXITING."
            return Route

        # there was a response
        if res is not None:
            tryCount = 0
            # has this address been visited before? then exit
            if contains(res.src, Route.getPath()):
                if verbose:
                    print "REPEAT NODE: EXITING."
                return Route
            # otherwise add the address to the list of visited addresses and continue
            if verbose:
                print "Successful TCP Ping on Hop number {}.".format(nextHop)
            Route.add(res.src)
            nextHop += 1

def publicGatewayPath(netAddr, verbose=False):
    if verbose:
        print "Pinging internet at {}".format(netAddr)
    # ping a net address and record the hops
    deepRoute = routeTracer(dest=netAddr.split("//")[1].split("/")[0], verbose=verbose)
    deepPath = deepRoute.getPath()

    # phone home to netAddr to get public ip of private network
    #  (it is assumed that IFF netAddr is not reachable, then the internet is not reachable)
    #  (it is assumed that IFF netAddr is reachable, then the public IP is known)
    PublicIP = publicIP(verbose=False, getIdentityAddr=netAddr)
    shallowPath = []

    # if a public IP was obtained for the private network
    if PublicIP is not None:
        if verbose:
            print "Pinging net's public iface as ....".format(PublicIP)
        # routeTracer the public IP to help isolate the private network hops
        #  from deepRoute
        shallowRoute = routeTracer(dest=PublicIP, verbose=verbose)
        shallowPath = shallowRoute.getPath()
    # otherwise deepRoute must contain only private hops
    #  and a cross reference is not needed
    else:
        if verbose:
            print "NO ACCESS TO INTERNET!"
        shallowPath = deepPath

    if shallowPath == deepPath:
        Backbone = deepPath
    else:
        Backbone = []
        for i in range(0,len(shallowPath)):
            Backbone.append(str(deepPath[i]))
    return Backbone

if __name__ == "__main__":
    args = sys.argv[0:]
    Destination = args[1]
    print Destination
    Backbone = publicGatewayPath(verbose=True, netAddr=Destination)
    print "Private Path to Public Gateway:"
    print Backbone

 

console output for traceroute python script

Console Output

Hop_2:

def routeTracer(dest, verbose, nextHop=1):

The crux of functionality for this part of the script is that a TCP packet is built and used to repeatedly ping a destination.

A destination is provided to the method (a URL), which is then passed to an IP header as the destination or ‘dst’, in scapy terminology. Notice the ‘ttl’ field along with the ‘dst’ field when building the IP header with scapy. It stands for Time to Live. The ttl for a packet is how many times the packet may be routed until it is automatically dropped. When a packet times out (the ttl is decremented each time it is routed), an error packet is sent back to the dropped packet’s source.

By having a packet incrementally time out on the way to a destination (you set ttl=1 to time out at the first router, set the ttl=2 to time out at the second router, etc), then you can receive packets from each hop/router along the path. You can get the source IP address from each of those response packets, the IP addresses of the hops/routers.

This is how routeTracer and traceroute essentially work, you systematically design your TCP/IP packets to time out for one extra hop each time you ping your destination (until you either reach your destination or are repeatedly not progressing along the path). This is how these tools give you the network path from your device to the input destination.

(Scapy provides much more functionality for engineering packets, but this is all that is required for this application.)

Hop_3:

def publicGatewayPath(netAddr, verbose=False):

The crux of functionality for this part of the script is that routeTracer is used on two targets (one target is the URL passed to the program, when called via CLI, and the other target is retrieved from the URL’s server). Based upon the results of those two routeTracer calls, the final array containing the private path to the public gateway can be created.

The first step is to obtain a route by pointing routeTracer to the first target (the URL). This path is stored as deepPath (we’ll need this for later).

Next, a subroutine publicIP gets the private network’s public IP address (the internet-facing IP for the gateway). The private network’s public IP address is obtained by communicating with the server at that URL. The way this works is that the server sends a response packet which contains the source IP address of the packet sent to it (there are many such services, here(https://stackoverflow.com/questions/9481419/how-can-i-get-the-public-ip-using-python2-7) is the resource I used to make a decision for what to use). It’s like if you sent a letter to your friend and they sent a letter back with your address on a piece of paper inside the envelope.

This sounds redundant, but remember that the server will see the public IP as the source of the packet and not the internal private IP (that public IP is what we want for this step). It’s also similar to if this script went on Google and typed “what is my IP” into the search; the point being an outside server is necessary to know your public IP.

If a public IP is not obtained from the call to the server, it is assumed that the private network is not connected to the public network (not connected to the internet). This, logically, means the original routeTracer path, deepPath, must contain the path we want. The original problem was that deepPath contains extra IP addresses outside of the private network. If the private network isn’t connected to the public network, then deepPath doesn’t contain IP addresses outside of the private network and we’re done (and there is much rejoicing!).

Otherwise…if we do have a response from the server (if we are connected to the internet)…(the rejoicing has to wait, but just for a bit)

We use routeTracer again, but this time routeTracer targets the public IP we obtained from the server. The point of this is to get a path, shallowPath, that terminates at the public gateway. You may be asking why we bother with deepPath if this shallowPath gets us exactly what we want (a path that terminates at the gateway)? That is because the last IP address in shallowPath is the external IP of the public gateway (the last element has the wrong IP for our purposes). And deepPath has the right IP’s, but it includes too many addresses — public addresses — and we don’t know where the cutoff is.

It is true that there are standards for public and private IP addresses, but the purpose of this script was to obtain this path algorithmically (and this publicGatewayPath algorithm is just one way to do it).

The final step is to match up the two paths (the arrays deepPath and shallowPath) and create a final array. The final array contains all of the IP addresses in shallowPath, but the last element/IP address is replaced by the correspondingly indexed element/IP address in deepPath.

deepPath = ["192.168.1.1", "10.0.0.1", "IP_of_ISP_device", "...", "URL_destination"]
shallowPath = ["192.168.1.1", "public_gateway_external_IP"]
Backbone_final_result = ["192.168.1.1", "10.0.0.1"]

…Hop_n:

The two major algorithms in this traceroute script are: routeTracer and publicGatewayPath. The routeTracer algorithm (traceroute equivalent) utilizes ‘ttl’ to incrementally obtain the IP addresses of the routers between the executing host and a destination.

The publicGatewayPath algorithm utilizes routeTracer to obtain two paths. The first is essentially any path through the public gateway (we use the passed URL, but any address not in the private network is fine), and the second path is to the public IP of the gateway (because we can know that external IP via help from an external server — and if we can’t get help from that external server, then the first path is what we wanted to begin with). We can then cross reference these two paths — one is too long with the right IP’s and the other is the right length where the final IP is incorrect.

Leave a Reply

Your email address will not be published. Required fields are marked *