ping.py – Python Implementation of the ping command
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.
Related Articles
- Sockets Programming In Python
- Python IRC Bot
- Python Crash Course
- Use Python to get the web page data in Epiphany
- Descramble Passwords from gftp Bookmarks using Python
Tags: Networking, Python

October 30th, 2009 at 11:52 pm
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
October 31st, 2009 at 3:46 am
Hi, there’s an implementation of the ICMP ping via impacket at core security.
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.
October 31st, 2009 at 2:51 pm
@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.
November 5th, 2009 at 6:56 pm
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?
November 5th, 2009 at 7:07 pm
I ran this with tcpdump. I see echo requests, but no replies. Any idea why?
November 5th, 2009 at 11:31 pm
@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.
November 6th, 2009 at 3:43 pm
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.
November 6th, 2009 at 4:06 pm
@Chris: Interesting. I had tried it on Windows only, since that was the only thing I had available at that moment.
November 6th, 2009 at 5:10 pm
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.
November 7th, 2009 at 1:42 pm
@Chris: The time.clock() function behaves differently under Windows and Linux:
I also suspect the following block of code
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.
November 8th, 2009 at 6:25 am
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.
November 10th, 2009 at 11:11 pm
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?
November 12th, 2009 at 12:39 pm
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.
November 17th, 2009 at 7:34 pm
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.
November 18th, 2009 at 6:08 pm
@James: Your explanation was helpful for me and will probably be for all the readers of this post. Thanks for writing it.