Question

I am trying to implement an ICMP based Traceroute in Python. I found a very helpful guide ( https://blogs.oracle.com/ksplice/entry/learning_by_doing_writing_your ) that has allowed me to create a UDP based Traceroute so just needs modification. However I have looked around and am having trouble changing the send socket and making it work. Is anybody able to assist me?

 #!/usr/bin/python

import socket

def main(dest_name):
    dest_addr = socket.gethostbyname(dest_name)
    port = 33434
    max_hops = 30
    icmp = socket.getprotobyname('icmp')
    udp = socket.getprotobyname('udp')
    ttl = 1
    while True:
        recv_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
        send_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, udp)
        send_socket.setsockopt(socket.SOL_IP, socket.IP_TTL, ttl)
        recv_socket.bind(("", port))
        send_socket.sendto("", (dest_name, port))
        curr_addr = None
        curr_name = None
        try:
            _, curr_addr = recv_socket.recvfrom(512)
            curr_addr = curr_addr[0]
            try:
                curr_name = socket.gethostbyaddr(curr_addr)[0]
            except socket.error:
                curr_name = curr_addr
        except socket.error:
            pass
        finally:
            send_socket.close()
            recv_socket.close()

        if curr_addr is not None:
            curr_host = "%s (%s)" % (curr_name, curr_addr)
        else:
            curr_host = "*"
        print "%d\t%s" % (ttl, curr_host)

        ttl += 1
        if curr_addr == dest_addr or ttl > max_hops:
            break

if __name__ == "__main__":
    main('google.com')
Was it helpful?

Solution 3

Ended up writing my own using scapy as this wasn't possible.

OTHER TIPS

Not sure why you chose scapy (nice module though it is), as this is certainly possible using only python. To send an ICMP packet, you simply send out your recv_socket. In order to send out this socket, you'll need to first create an ICMP packet.

However, it seems what you want is to send out a UDP packet over an ICMP socket. This won't work as you might think.

First, let me say apparently there exists a patch to the Linux kernel that'll allow SOCK_DGRAM with IPPROTO_ICMP: ICMP sockets (linux). I've not tested this.

Generally, though, this combination of socket flags won't work. This is because an ICMP socket expects an ICMP header. If you were to send an empty string over recv_socket similarly to send_socket, the kernel will drop the packet. Further, if you were to layer a UDP header over an ICMP header, the receiving system will only react to the received ICMP header, and treat the UDP header as nothing more than just data appended to ICMP. In fact, in its ICMP reply to you, the remote system will include your UDP header simply as data you sent to it in the first place.

The reason you're able to send an empty string over send_socket is because the kernel has created the UDP header for you, and what you send over that UDP socket is simply appended as data to the UDP header. It's not like that with an ICMP socket. As I wrote, you'll need to create the icmp header to send on this socket.

The actuality of what happens in a UDP "ping" is this: A UDP packet is sent over a UDP socket to a remote system, using a (hopefully) unopened port as the destination port. This elicits an ICMP response back from the remote system (type 3, code 3). At this point, you will need an ICMP-registered socket to handle the ICMP reply back, which requires root (or administrator on Windows) privileges.

To create an icmp echo header is quite easy:

import struct
icmp = struct.pack(">BBHHH", 8, 0, 0, 0, 0)
icmp = struct.pack(">BBHHH", 8, 0, checksum(icmp), 0, 0)

A couple things to note with this header. First, arguments four and five are the "Identifier" and "Sequence Number" fields. Typically the identifier is set to a process ID while the sequence number starts at 1 so you can track the sequence of sends to replies. In this example, I've set the numbers to zero just for illustration (although, it is perfectly normal to keep it this way if you don't care about them).

Second, see the checksum(icmp) function I allude to? I'm assuming you have access to code that does this. It is known as the 1's complement checksum, and is ratified under RFC 1071. The checksum is vital because, without it being correctly computed, the receiving system might not forward the packet to any of the ICMP socket handlers.

Third, if you're wondering why I initially created the header with a zero checksum, it's because the kernel bases a checksum result on that field being zero, before the actual checksum is created and added back to the header.

Because the struct module deals with what's called "packed" binary data, I advise that you familiarize yourself with bit shifting and bitwise operators. You're going to need this as you advance further into raw sockets, particulary if and when you need to extract certain bits from bit fields that may or may not overlap. For example, the following is the IP header:

    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 
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 0 |Version|  IHL  |Type of Service|          Total Length         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 4 |         Identification        |Flags|      Fragment Offset    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 8 |  Time to Live |    Protocol   |         Header Checksum       |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
12 |                       Source Address                          |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
16 |                    Destination Address                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
20 |                    Options                    |    Padding    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Assuming you wanted to insert IP address 192.168.1.10 in the source field, first you must note its length, 4 bytes. To correctly insert this into struct (just this field, not rest of header, as an example), you must:

struct.pack(">I", 192 << 24| 168 << 16| 1 << 8| 10)

Doing this adds the proper integer for this field, 3232235786L.

(Some readers may point out that the same result could be had with struct.pack(">BBBB", 192, 168, 1, 10). Although this would work for this use case, it is incorrect in general. In this case, it works because an IP address is byte-oriented; however, fields that are not byte-oriented, such as the checksum field, will fail because the resulting value of a checksum is greater than 255, that is to say, it is expected to be a 16-bit value. So don't do this in general i.e. use the exact bits expected by a protocol field.)

As another example for learning bit shifting and operations, take the VER field of the IP header. This is a nibble-sized field, i.e. 4 bits. In order to extract this, you would do the following as an example (assuming IHL is zero):

# Assume ip[] already has a full IP header.
ver = struct.unpack("!B", ip[0])[0]

# This produces the integer 4, which is required for sending IPV4
ver >> 4 

In a nutshell, I've right-shifted a one-byte value down 4 bits. In this downshift, the new value compared to the old value now looks like this:

# OLD VALUE IN BINARY; value is 64
# 01000000

# NEW VALUE IN BINARY; value is 4
# 00000100

It's important to note that, without this shift, checking the raw binary value would result in 64, which isn't what is expected. For the inverse, in order to "pack" the value 4 into the upper 4 bits of one byte, do this:

# New value will be 64, or binary 01000000
x = 4 << 4

Hope this points you in the right direction.

Old question, but adding another point for clarity as I recently had to do just this.

You can write a native Python version of ICMP traceroute using sockets. The two important issues to solve are:

  1. creating the ICMP header with the correct checksum (as Eugene states above)
  2. setting the TTL of the socket using sockopts

For the first issue, there's a great example in the pyping module that works perfectly; it's what I used when I wrote my traceroute utility.

For the second, it's as easy as:

current_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, 
                               socket.getprotobyname("icmp"))
current_socket.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, ttl)

where 'ttl' is the integer value of the TTL you're setting

Then it's just a matter of doing a send/recv and watching the type/code in the return packets, in a control structure that increments the TTL.

I modeled my header pack/unpack after what was already written in the pyping module; just look for header type = 11 with code 0 for the intermediate hops, and header type = 0 for when you've reached your destination.

Scapy works fine, but it's slower, and is an additional external dependency. I was able to write a robust traceroute in an afternoon with AS-lookup (via RADb) and geolocation, using nothing but sockets.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top