«
»

ping.py – Python Implementation of the ping command

October 30th, 2009 by George Notaras

I’ve been looking for a pure python implementation of the ping command. Now that I found one, I am not sure if I want to use it, as it has a restriction: only privileged users can ping other hosts. I’ve used the ping command successfully as a normal user on all operating systems I have tried and never had an issue. Currently, I do not have the time to investigate this limitation, but, judging by the exception I get, it has to do with the creation of the socket through which the ICMP packet is sent. The normal user does not have the required privileges to create this socket.

[This post has been updated]

[Update #1]: This limitation also exists on the ping command, which runs with setuid access rights. (Thanks Stephane)

[Update #2]: After Chris Hallman reported that the original code actually works on Windows only, I made some changes to it and publish the updated code below.

The following code is a pure Python implementation of the ping command. I originally found it in the source code tree of pylucid in a subdirectory where the developers keep various code snippets.

I had originally tested the code under Microsoft Windows as this was the OS I had available at the moment. Chris Hallman noticed that the code does not work under GNU/Linux. After spending some hours trying to figure out what was wrong it, I realized that the part of the code that caused the failure on Linux was the use of the time.clock() function instead of time.time(). The clock() method works differently under Windows and Linux.

I fixed the 2007 implementation and hereby publish the updated code(as python-ping). Feel free to test it and report any issues.

Needed: test on Solaris.

#!/usr/bin/env python
 
"""
    A pure python ping implementation using raw socket.
 
 
    Note that ICMP messages can only be sent from processes running as root.
 
 
    Derived from ping.c distributed in Linux's netkit. That code is
    copyright (c) 1989 by The Regents of the University of California.
    That code is in turn derived from code written by Mike Muuss of the
    US Army Ballistic Research Laboratory in December, 1983 and
    placed in the public domain. They have my thanks.
 
    Bugs are naturally mine. I'd be glad to hear about them. There are
    certainly word - size dependenceies here.
 
    Copyright (c) Matthew Dixon Cowles, <http://www.visi.com/~mdc/>.
    Distributable under the terms of the GNU General Public License
    version 2. Provided with no warranties of any sort.
 
    Original Version from Matthew Dixon Cowles:
      -> ftp://ftp.visi.com/users/mdc/ping.py
 
    Rewrite by Jens Diemer:
      -> http://www.python-forum.de/post-69122.html#69122
 
    Rewrite by George Notaras:
      -> http://www.g-loaded.eu/2009/10/30/python-ping/
 
    Revision history
    ~~~~~~~~~~~~~~~~
 
    November 8, 2009
    ----------------
    Improved compatibility with GNU/Linux systems.
 
    Fixes by:
     * George Notaras -- http://www.g-loaded.eu
    Reported by:
     * Chris Hallman -- http://cdhallman.blogspot.com
 
    Changes in this release:
     - Re-use time.time() instead of time.clock(). The 2007 implementation
       worked only under Microsoft Windows. Failed on GNU/Linux.
       time.clock() behaves differently under the two OSes[1].
 
    [1] http://docs.python.org/library/time.html#time.clock
 
    May 30, 2007
    ------------
    little rewrite by Jens Diemer:
     -  change socket asterisk import to a normal import
     -  replace time.time() with time.clock()
     -  delete "return None" (or change to "return" only)
     -  in checksum() rename "str" to "source_string"
 
    November 22, 1997
    -----------------
    Initial hack. Doesn't do much, but rather than try to guess
    what features I (or others) will want in the future, I've only
    put in what I need now.
 
    December 16, 1997
    -----------------
    For some reason, the checksum bytes are in the wrong order when
    this is run under Solaris 2.X for SPARC but it works right under
    Linux x86. Since I don't know just what's wrong, I'll swap the
    bytes always and then do an htons().
 
    December 4, 2000
    ----------------
    Changed the struct.pack() calls to pack the checksum and ID as
    unsigned. My thanks to Jerome Poincheval for the fix.
 
 
    Last commit info:
    ~~~~~~~~~~~~~~~~~
    $LastChangedDate: $
    $Rev: $
    $Author: $
"""
 
 
import os, sys, socket, struct, select, time
 
# From /usr/include/linux/icmp.h; your milage may vary.
ICMP_ECHO_REQUEST = 8 # Seems to be the same on Solaris.
 
 
def checksum(source_string):
    """
    I'm not too confident that this is right but testing seems
    to suggest that it gives the same answers as in_cksum in ping.c
    """
    sum = 0
    countTo = (len(source_string)/2)*2
    count = 0
    while count<countTo:
        thisVal = ord(source_string[count + 1])*256 + ord(source_string[count])
        sum = sum + thisVal
        sum = sum & 0xffffffff # Necessary?
        count = count + 2
 
    if countTo<len(source_string):
        sum = sum + ord(source_string[len(source_string) - 1])
        sum = sum & 0xffffffff # Necessary?
 
    sum = (sum >> 16)  +  (sum & 0xffff)
    sum = sum + (sum >> 16)
    answer = ~sum
    answer = answer & 0xffff
 
    # Swap bytes. Bugger me if I know why.
    answer = answer >> 8 | (answer << 8 & 0xff00)
 
    return answer
 
 
def receive_one_ping(my_socket, ID, timeout):
    """
    receive the ping from the socket.
    """
    timeLeft = timeout
    while True:
        startedSelect = time.time()
        whatReady = select.select([my_socket], [], [], timeLeft)
        howLongInSelect = (time.time() - startedSelect)
        if whatReady[0] == []: # Timeout
            return
 
        timeReceived = time.time()
        recPacket, addr = my_socket.recvfrom(1024)
        icmpHeader = recPacket[20:28]
        type, code, checksum, packetID, sequence = struct.unpack(
            "bbHHh", icmpHeader
        )
        if packetID == ID:
            bytesInDouble = struct.calcsize("d")
            timeSent = struct.unpack("d", recPacket[28:28 + bytesInDouble])[0]
            return timeReceived - timeSent
 
        timeLeft = timeLeft - howLongInSelect
        if timeLeft <= 0:
            return
 
 
def send_one_ping(my_socket, dest_addr, ID):
    """
    Send one ping to the given >dest_addr<.
    """
    dest_addr  =  socket.gethostbyname(dest_addr)
 
    # Header is type (8), code (8), checksum (16), id (16), sequence (16)
    my_checksum = 0
 
    # Make a dummy heder with a 0 checksum.
    header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, my_checksum, ID, 1)
    bytesInDouble = struct.calcsize("d")
    data = (192 - bytesInDouble) * "Q"
    data = struct.pack("d", time.time()) + data
 
    # Calculate the checksum on the data and the dummy header.
    my_checksum = checksum(header + data)
 
    # Now that we have the right checksum, we put that in. It's just easier
    # to make up a new header than to stuff it into the dummy.
    header = struct.pack(
        "bbHHh", ICMP_ECHO_REQUEST, 0, socket.htons(my_checksum), ID, 1
    )
    packet = header + data
    my_socket.sendto(packet, (dest_addr, 1)) # Don't know about the 1
 
 
def do_one(dest_addr, timeout):
    """
    Returns either the delay (in seconds) or none on timeout.
    """
    icmp = socket.getprotobyname("icmp")
    try:
        my_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
    except socket.error, (errno, msg):
        if errno == 1:
            # Operation not permitted
            msg = msg + (
                " - Note that ICMP messages can only be sent from processes"
                " running as root."
            )
            raise socket.error(msg)
        raise # raise the original error
 
    my_ID = os.getpid() & 0xFFFF
 
    send_one_ping(my_socket, dest_addr, my_ID)
    delay = receive_one_ping(my_socket, my_ID, timeout)
 
    my_socket.close()
    return delay
 
 
def verbose_ping(dest_addr, timeout = 2, count = 4):
    """
    Send >count< ping to >dest_addr< with the given >timeout< and display
    the result.
    """
    for i in xrange(count):
        print "ping %s..." % dest_addr,
        try:
            delay  =  do_one(dest_addr, timeout)
        except socket.gaierror, e:
            print "failed. (socket error: '%s')" % e[1]
            break
 
        if delay  ==  None:
            print "failed. (timeout within %ssec.)" % timeout
        else:
            delay  =  delay * 1000
            print "get ping in %0.4fms" % delay
    print
 
 
if __name__ == '__main__':
    verbose_ping("heise.de")
    verbose_ping("google.com")
    verbose_ping("a-test-url-taht-is-not-available.com")
    verbose_ping("192.168.1.1")

I decided not to publish the updated code on CodeTRAX, where I normally publish any programming-related stuff, but leave it on G-Loaded instead.

The ping.py – Python Implementation of the ping command by George Notaras, unless otherwise expressly stated, is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License. Terms and conditions beyond the scope of this license may be available at www.g-loaded.eu.

Related Articles

Tags: ,

Bookmark and Share

15 Responses to “ping.py – Python Implementation of the ping command”

  1. Stephane Says :

    roadrunner /tmp $ ls -l `which ping`
    -rws–x–x 1 root root 30532 2009-07-25 19:03 /bin/ping

    yeah, ping is setuid root, so it runs with root privileges no matter who runs it.

    Stephane

  2. Lee Says :

    Hi, there’s an implementation of the ICMP ping via impacket at core security.

    #!/usr/bin/python
    # Copyright (c) 2003 CORE Security Technologies
    #
    # This software is provided under under a slightly modified version
    # of the Apache Software License. See the accompanying LICENSE file
    # for more information.
    #
    # $Id: ping.py,v 1.3 2003/10/27 17:36:56 jkohen Exp $
    #
    # Simple ICMP ping.
    #
    # This implementation of ping uses the ICMP echo and echo-reply packets
    # to check the status of a host. If the remote host is up, it should reply
    # to the echo probe with an echo-reply packet.
    # Note that this isn't a definite test, as in the case the remote host is up
    # but refuses to reply the probes.
    # Also note that the user must have special access to be able to open a raw
    # socket, which this program requires.
    #
    # Authors:
    #  Gerardo Richarte <gera@coresecurity.com>
    #  Javier Kohen <jkohen@coresecurity.com>
    #
    # Reference for:
    #  ImpactPacket: IP, ICMP, DATA.
    #  ImpactDecoder.
     
    import select
    import socket
    import time
    import sys
     
    from impacket import ImpactDecoder, ImpactPacket
     
    if len(sys.argv) < 3:
    	print "Use: %s <src ip> <dst ip>" % sys.argv[0]
    	sys.exit(1)
     
    src = sys.argv[1]
    dst = sys.argv[2]
     
    # Create a new IP packet and set its source and destination addresses.
     
    ip = ImpactPacket.IP()
    ip.set_ip_src(src)
    ip.set_ip_dst(dst)
     
    # Create a new ICMP packet of type ECHO.
     
    icmp = ImpactPacket.ICMP()
    icmp.set_icmp_type(icmp.ICMP_ECHO)
     
    # Include a 156-character long payload inside the ICMP packet.
    icmp.contains(ImpactPacket.Data("A"*156))
     
    # Have the IP packet contain the ICMP packet (along with its payload).
    ip.contains(icmp)
     
    # Open a raw socket. Special permissions are usually required.
    s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
    s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
     
    seq_id = 0
    while 1:
    	# Give the ICMP packet the next ID in the sequence.
    	seq_id += 1
    	icmp.set_icmp_id(seq_id)
     
    	# Calculate its checksum.
    	icmp.set_icmp_cksum(0)
    	icmp.auto_checksum = 1
     
    	# Send it to the target host.
    	s.sendto(ip.get_packet(), (dst, 0))
     
    	# Wait for incoming replies.
    	if s in select.select([s],[],[],1)[0]:
    	   reply = s.recvfrom(2000)[0]
     
    	   # Use ImpactDecoder to reconstruct the packet hierarchy.
    	   rip = ImpactDecoder.IPDecoder().decode(reply)
    	   # Extract the ICMP packet from its container (the IP packet).
    	   ricmp = rip.child()
     
    	   # If the packet matches, report it to the user.
    	   if rip.get_ip_dst() == src and rip.get_ip_src() == dst and icmp.ICMP_ECHOREPLY == ricmp.get_icmp_type():
    		   print "Ping reply for sequence #%d" % ricmp.get_icmp_id()
     
    	   time.sleep(1)

    basically impacket does all of the header packing and can chain the different levels of the payload. More on impacket can be found via http://oss.coresecurity.com/projects/impacket.html – Very handy and saves you lots of time when you need to write custom payloads.

  3. George Notaras Says :

    @Stephane: Never thought to check the permissions of the actual ping command. Thanks for pointing this out.

    @Lee: Thanks for bringing this module and the ping implementation based on that into my attention. Looks great. I had to alter the pasted code in your comment in order to add the license info in the header and apply syntax highlighting. I hope this is OK.

  4. chris hallman Says :

    I tried this and the delay is always 0, but I know better:

    [root@panma021 python]# python icmp.py
    ping a46dcorr01… get ping in 0.0000ms
    ping a46dcorr01… get ping in 0.0000ms
    ping a46dcorr01… get ping in 0.0000ms
    ping a46dcorr01… get ping in 0.0000ms

    [root@panma021 python]# ping a46dcorr01
    PING a46dcorr01.example.org (123.123.123.123) 56(84) bytes of data.
    64 bytes from A46DCORR01.EXAMPLE.ORG (123.123.123.123): icmp_seq=1 ttl=249 time=17.0 ms
    64 bytes from A46DCORR01.EXAMPLE.ORG (123.123.123.123): icmp_seq=2 ttl=249 time=23.5 ms
    64 bytes from A46DCORR01.EXAMPLE.ORG (123.123.123.123): icmp_seq=3 ttl=249 time=17.1 ms
    64 bytes from A46DCORR01.EXAMPLE.ORG (123.123.123.123): icmp_seq=4 ttl=249 time=17.3 ms

    Is this an intended result or a bug?

  5. chris hallman Says :

    I ran this with tcpdump. I see echo requests, but no replies. Any idea why?

  6. George Notaras Says :

    @Chris: It worked here as expected. BTW, I had to remove the domain names and IP addresses from your first comment. No references to online stores are allowed. Sorry.

  7. chris hallman Says :

    It’s a brick and mortar store. Anyway, it seems to work on Windows, but not on Linux. I was trying it on Linux originally.

  8. George Notaras Says :

    @Chris: Interesting. I had tried it on Windows only, since that was the only thing I had available at that moment.

  9. chris hallman Says :

    I really would like to get this working on Linux also so I can do some ICMP testing. I’ve reviewed the packet captures from Linux & Windows and the only difference I can see is the identification field in the IP header. On Linux it’s 0, yet Windows has an incrementing value. I’ve tried modifying the code you posted, but can’t quite figure out how to add an incrementing ID. The ‘my_ID’ variable in the program is the identification field in the ICMP header, therefore it’s not related to what I’m trying to change.

  10. George Notaras Says :

    @Chris: The time.clock() function behaves differently under Windows and Linux:

    time.clock()

    On Unix, return the current processor time as a floating point number expressed in seconds. The precision, and in fact the very definition of the meaning of “processor time”, depends on that of the C function of the same name, but in any case, this is the function to use for benchmarking Python or timing algorithms.

    On Windows, this function returns wall-clock seconds elapsed since the first call to this function, as a floating point number, based on the Win32 function QueryPerformanceCounter(). The resolution is typically better than one microsecond.

    I also suspect the following block of code

    if packetID == ID:
        bytesInDouble = struct.calcsize("d")
        timeSent = struct.unpack("d", recPacket[28:28 + bytesInDouble])[0]
        print "timeReceived:", timeReceived, " -- ", "timeSent:", timeSent     ## ADDED BY GNOT
        return timeReceived - timeSent

    I added the line that is marked with the “ADDED BY GNOT” comment. The output shows that under linux the precision of the printed times is not adequate to calculate the ping delay.

  11. George Notaras Says :

    The problem with the code was the use of the time.clock() function. I fixed it and corrected the code that was published in the post above.

  12. chris hallman Says :

    You sir, are brilliant!

    I understand the change, however I don’t understand how it corrected operation of the program. Initially, tcpdump showed packets were being sent but no responses were received and the resulting RTT was 0.0ms. Now, I see requests, responses and accurate RTT. How did your change correct all this?

  13. George Notaras Says :

    Hi Chris, I really do not have an explanation about what you describe. I suspect that this happened because the time.clock() function was also used when attaching the timestamp to the ICMP packet just before sending, which, when done on Linux, does not provide adequate time resolution in order to calculate the time difference. But that’s just a wild guess.

  14. James Says :

    Just so you know all ping commands have the restriction. If you do an ls -l on the ping command on any *nix system, you will find the following

    -rwsr-xr-x 1 root root 35108 Jun 15 2004 /bin/ping

    Note the s in the permissions. Ping is setUID root. This allows unprivileged users the ability to use ping. Why this is done is because ping (ICMP) require that the interface be placed into a listening mode, which requires root privileges to do. Additionally ICMP itself is a low level protocol requiring root privilege. With ping you are working at the hardware level. With something like a web browser you are working much higher in the food chain.

    Is this a security problem, yes, which is why your python ping requires root. This is also why systems using busybox usually don’t allow normal users to do ping as setting ping setUID root can set all of the busybox commands setUID root.

  15. George Notaras Says :

    @James: Your explanation was helpful for me and will probably be for all the readers of this post. Thanks for writing it.

Comments are automatically disabled after a certain period of time. Further discussion about the published content is still possible though in the G-Loaded Forums.