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.
[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.
ping.py – Python Implementation of the ping command by George Notaras is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
Copyright © 2009 - Some Rights Reserved
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
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.
@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.
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?
I ran this with tcpdump. I see echo requests, but no replies. Any idea why?
@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.
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.
@Chris: Interesting. I had tried it on Windows only, since that was the only thing I had available at that moment.
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.
@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.
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.
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?
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.
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.
@James: Your explanation was helpful for me and will probably be for all the readers of this post. Thanks for writing it.
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.
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.
For thread safety, the socket functions need to be mutexed (except for the select() call), at least on Win32.
That’s interesting code…
Unfortunately, there seems to be an error on line 95:
while count> 16) + (sum & 0xffff)
-Ken
@Ken Corey: Problem caused by pasting the code without first converting to valid HTML. Copy and Paste from HTML source instead.
Here’s a version that works under Python 3.2:
Received this email by Jim B, and I thought it would be useful to contribute it to the discussion:
@Zach Ware: Thanks for porting the code to Python 3.
So how do I go about changing the packet size of the ping request? Is it this bit here –
data = (192 - bytesInDouble) * "Q"
@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.
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.
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.
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 download the ZIP file from: http://www.codetrax.org/projects/python-ping/files
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. ;)
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.
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.
Finally, I have re-activated the WP-Syntax plugin. I hope all issues with character escaping are now resolved.
I merged your changes and martin’s changes and updated my repo: (mod: link is invalid)
@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.
@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?
@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).
(FYI, my 5:02pm note above is @Jens, regarding the changes he merged)
@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 :)
George, I’m assuming this does not work with IPv6. Do you have any plans to port this code to support ICMPv6?
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.
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.]
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.
A zip archive can be found in the files section of the following development website:
http://www.codetrax.org/projects/python-ping/files
How can I use the code for sub-domains ? I have example.org/testing and It returns ‘failed’. (socket error: ‘getaddrinfo failed’)
Kindly help me in this.
@Ravi
ping does not accept URLs as argument. You have to use a hostname or an IP address. In the case of your subdomain use:
ping example.org
In checksum() —
> # Swap bytes. Bugger me if I know why.
> answer = answer >> 8 | (answer << 8 & 0xff00)
It's because your checksum algorithm explicitly does a big-endian computation. You must have done this on x86 or x86-64 architectures, which are little-endian. So the swap just converts your value to little-endian.
Testable: if this fails on a big-endian Sparc architecture (older Solaris…) but works without the swap, then there you go. I wish I had a Sparc to try it out on.
Hello, does anyone know how to set a source address for the ping packets?
I want to add to this ping implementation the equivalent to the -S option of the original ping. Anyone?
Thanks
thanks for the post, i wanted to make one observation. the way you calculate the rtt is not the same as ping. from what i can tell you the the get the send time when sending a ping and and the recive time when you recive the ping. you then subtract one from the other to get the rtt. however ping.c sends the send time as the data payload and uses the value it gets pack in the echo response (not the value it has in memory) to calculate the rtt.
im still trying to work out if/why this bothers me :). however thought i would point it out regardless
Bobmon is correct WRT to endianness. Network numerical types are all big-endian. x86 and x86_64 are little endian architectures so you need to swap byte order for all numbers you send and receive on the network.
In C, we handle these with the hton* and ntoh* family of functions. These are available in python socket library, but you can use the ‘!’ modifier on your integers in your struct.pack()s and unpack()s to explicitly use the network order, which is probably a little more pythonic.
Hello RK,
Thanks for your feedback. Currently it is impossible for me to further contribute to this project. Pierre Bourdon had taken over in 2010 and is currently maintaining the ping library in PyPI. Please contact Pierre regarding contributions.
Kind Regards,
George