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
- Descramble Passwords from gftp Bookmarks using Python
- Python IRC Bot
- Use Python to get the web page data in Epiphany
- Python Crash Course
Tags: Networking, Python, Snippet
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.
June 22nd, 2010 at 2:49 pm
Hello,
It seems you send every packet with the same id and sequence number.
This results in incorrect response times when short timeouts are used since no packets are discarded.
It would be a good idea to increment sequence numbers and also replace:
my_ID = os.getpid() & 0xFFFF
with something like:
my_ID = int(time.time() * 100000) & 0xFFFF
so that same ID is not produced each time.
December 9th, 2010 at 2:32 am
I found that I needed to make a change to the program to make it run on HP-UX – a change which may also be necessary for portability to other platforms. The line which reads:
“bbHHh”, ICMP_ECHO_REQUEST, 0, socket.htons(my_checksum), ID, 1
had to be changed to:
“bbHHh”, ICMP_ECHO_REQUEST, 0, socket.htons(my_checksum)&0xffff, ID, 1
The reason why the change is necessary is that Python doesn’t support unsigned integers, so you can sometimes end up with negative numbers when you rearrange bits. On Linux, this change has no effect.
March 12th, 2011 at 7:28 am
For thread safety, the socket functions need to be mutexed (except for the select() call), at least on Win32.
July 25th, 2011 at 7:03 pm
That’s interesting code…
Unfortunately, there seems to be an error on line 95:
while count> 16) + (sum & 0xffff)
-Ken
July 27th, 2011 at 7:25 pm
@Ken Corey: Problem caused by pasting the code without first converting to valid HTML. Copy and Paste from HTML source instead.
August 4th, 2011 at 12:29 am
Here’s a version that works under Python 3.2:
August 7th, 2011 at 3:53 pm
Received this email by Jim B, and I thought it would be useful to contribute it to the discussion:
August 7th, 2011 at 3:54 pm
@Zach Ware: Thanks for porting the code to Python 3.
September 2nd, 2011 at 5:08 pm
So how do I go about changing the packet size of the ping request? Is it this bit here -
data = (192 - bytesInDouble) * "Q"
September 6th, 2011 at 2:34 am
@George: Thanks for the original post! This is what I wanted to do when I first dove into Python, only to find that pinging is surprisingly difficult to do permissions-wise (it seemed so basic… but writing a kluge to invoke the system ‘ping’ regularly wasn’t a reasonable option so I’m glad to see this in pure Python).
@Zach: Particular thanks for the Python 3.x version, as I’m working that far more than 2.x for my own scripts.
September 6th, 2011 at 2:43 am
Ouch… looks like @Zach’s code comment may have been mangled by the XHTML parser, with syntax errors apparently due to angle brackets in the code (gt, lt, that sort of thing) being interpreted as tags here.
September 6th, 2011 at 3:45 am
Actually, the original post is also corrupted… for example, “while count\> 16) + (sum & 0xffff)” is apparently missing some code. Again, it’s an HTML conversion problem.
September 6th, 2011 at 11:38 am
I am sorry for the inconvenience. I had uninstalled a syntax highlighter plugin from wordpress and it seems that this has caused the issue with escaping.
Please find the correct ping.py module from either of the following locations:
1) Download the ZIP file from: http://www.codetrax.org/projects/python-ping/files
2) Check out the code from the mercurial repository:
I always hated wordpress for this.
This post will corrected and the Python3 version will be added as soon as I find some free time to do this. Thanks for your understanding. ;)
September 6th, 2011 at 5:05 pm
I managed to parse out the bits that were missing (between examining the two versions, viewing the page sources (which had some of the missing info), and looking at the original ping.c source code), and I got both the 3.x and 2.x versions working as a baseline.
Then I did a whole lot of rework and updating last night and posted a new and improved Python 3.x version of this script on my site (with a link back to your page here):
http://www.falatic.com/index.php/39/pinging-with-python
While I focused on the Python 3 implementation, this should be pretty trivial to backport to 2.x. It seems to be quite robust, though I haven’t tested it on big-endian hardware (in theory it should work fine or at least require only minor tweaks for such a platform… I did my best to plan ahead for that use case).
Note: As for WordPress and source code, I’ve been using the “SyntaxHighlighter Evolved” plugin with good results.
September 7th, 2011 at 4:09 pm
Marty, thanks for your work. As for the syntax highlighter, I’d been using wp-syntax, but decided to get rid of it recently. Apparently this has not worked very well, so I might use a syntax highlighter again. BTW, “SyntaxHighlighter Evolved” looks quite nice. Thanks.
September 8th, 2011 at 12:27 pm
Finally, I have re-activated the WP-Syntax plugin. I hope all issues with character escaping are now resolved.
September 12th, 2011 at 2:28 pm
I merged your changes and martin’s changes and updated my repo: http://github.com/jedie/python-code-snippet/blob/master/CodeSnippets/ping.py
September 12th, 2011 at 5:00 pm
@Martin, @Jens: Thanks for your work and contributions to this code. However, I have two remarks:
1) it seems you have focused on Python 3, which has not yet been adopted by the majority of users.
2) several changes to the original code have been introduced by your published snippets.
To be honest, despite the fact that Martin has built upon the source code of the ping utility that ships with cygwin, I’d rather stick with the old version for now (published in this post), which has been around for over a decade, until the new changes are reviewed by other users. Unfortunately, I do not have the time or the required expertise to evaluate them. Thanks for your understanding.
September 12th, 2011 at 5:02 pm
@Jens: I see that, though you primarily just backported my script to Python 2.x and removed all the delimiting comments I added for clarity. The only actual bugfix seems to be the signal handling modification – I assume that SIGBREAK caused some problem on Ubuntu?
September 12th, 2011 at 5:17 pm
@George: Just saw your note. I don’t expect you to merge in my modifications – that’s why I published them separately, though per GPL you’re welcome to whatever bits you think are helpful.
Yes, there are certainly changes to the code, as well as a more rigorous analysis of the checksum routine. My changes were not Cygwin-specific, it was just a handy version I could readily test against. Other than adding in the “average” ping time what I turned this into is pretty much the same as what the original public domain ping.c does.
Note that @Jens backport *is* the Python 2 version, for those using that (yes, I realize mine may have a more niche audience, though I think it’s always valuable to have a Python 3 version out there as people move forward… which is why I focused on the Python 3 port).
September 12th, 2011 at 5:19 pm
(FYI, my 5:02pm note above is @Jens, regarding the changes he merged)
September 12th, 2011 at 5:30 pm
@Marty: Thanks for your feedback and your work. As soon as I find some free time, I’ll go through your changes. They seem to be of high quality and I’m quite certain that studying your snippet will be quite educative about how pinging is performed.
PS: Note to self: It is about time I get python 3 installed in my system :)
October 7th, 2011 at 1:37 am
George, I’m assuming this does not work with IPv6. Do you have any plans to port this code to support ICMPv6?
October 7th, 2011 at 2:31 am
Hi Luiz.
Currently, I am out of free time and, also, I’m not sure I have the required qualifications for such a task. But it would be an interesting task for the future. Also, there is another python library I’d like to check out at some point, impacket, and also scapy. I am quite certain the latter has support for ICMPV6, but I’ve never checked it out.
October 12th, 2011 at 3:59 pm
I created a separate GIT Repository for ping.py here: [link removed. see note]
I found the existing fork [link removed. see note] and merged everything together. Would be great if everyone worked on the same repository.
A idea is to implement a subprocess call ping with a parser of the output, so that no admin right is needed: [link removed. see note]
[MODERATED: Please provide some explanation about where this code derives and what it has to do with the code that has been posted in this post. This is becoming really confusing and I cannot follow. Sorry.]
October 12th, 2011 at 5:18 pm
First of all, I’d like to thank you all for your ideas and code contributions. Also, I’d like to thank all those who have emailed me their ideas.
Unfortunately, this post is becoming more and more confusing for readers, since it is cumbersome to diff the code snippets provided in the comments or in any repositories linked from the comments.
So, this is how it will be resolved once and for all.
The code that has been posted above has been uploaded to the following repositories:
1) https://bitbucket.org/gnotaras/python-ping
2) https://github.com/gnotaras/python-ping
3) https://source.codetrax.org/hgroot/python-ping/
Also, a zip archive can be found in the files section of the following development website:
http://www.codetrax.org/projects/python-ping/files
If you have worked on the code that has been published above, it is highly recommended that you fork the repositories at bitbucket/github, commit your changes and then create a pull request. Also, feel free to start working on a specific fork of yours without expecting me to actually pull your changes.
The idea is to have a clear view of what changes have been made to the published code in this post. This way we will all save our free time and things will be generally better.
October 12th, 2011 at 5:20 pm
@Jens: You have forked an old version of the code that probably does not work on both Linux and Windows.
Note to all:
Please create a fork of the repo https://github.com/gnotaras/python-ping so as to be able to tell what changes you have applied using the above code as a starting point. And then feel free to work on your fork.
This is becoming really confusing for me and I do not have the time to browse through the commits to understand the modifications. This post is about reporting issues and modifications to the _published_ code.
Thank you all for your understanding.