3rd Feb 2017
Efficiently Pinging a Subnet
Two methods to use the ping
command to quickly scan a network
I often find that I want to list which IPs in a subnet are active, whether to find a free address to allocate to a machine, to verify that the correct number of machines are online, of for various other reasons.
The Bad Way
The most obvious choice is "ping -b 192.168.1.255
" - that is, a broadcast ping
to the broadcast address of the subnet. However,
that does not catch devices which are configured not to respond to broadcast ping
, and it can
take a long time to run. Mainly, though, the output is very messy to read. It doesn't just
give a nice clean list of IP addresses.
The next option is to ping
each in turn; this will pick up those machines which do not
respond to broadcast ping
packets, but it takes a while. However, the execution time
can be controlled somewhat
by the "ping -c1 -w1
" arguments. These say to send only one ping
packet,
and to only wait one second for it to receive a response:
$ for ip in `seq 1 254` > do > ping -c1 -w1 192.168.1.$ip > done PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data. --- 192.168.1.1 ping statistics --- 2 packets transmitted, 0 received, 100% packet loss, time 999ms PING 192.168.1.2 (192.168.1.2) 56(84) bytes of data. --- 192.168.1.2 ping statistics --- 1 packets transmitted, 0 received, 100% packet loss, time 0ms PING 192.168.1.3 (192.168.1.3) 56(84) bytes of data. --- 192.168.1.3 ping statistics --- 1 packets transmitted, 0 received, 100% packet loss, time 0ms PING 192.168.1.4 (192.168.1.4) 56(84) bytes of data. 64 bytes from 192.168.1.4: icmp_seq=1 ttl=64 time=2.13 ms --- 192.168.1.4 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 2.136/2.136/2.136/0.000 ms PING 192.168.1.5 (192.168.1.5) 56(84) bytes of data.
This does pick up more devices, but is still very slow (254 seconds to ping
192.168.1.1
to 192.168.1.254
), and the output is clearly very messy. So a better way is needed.
Version 1 - Just Class C
The first attempt runs like this when asked to ping
the 192.168.1.0/24
subnet - much better. And it completes in a fraction over one second:
$ ./ping1.sh 192.168.1 192.168.1.13 192.168.1.4 192.168.1.8 192.168.1.65 192.168.1.254 192.168.1.88 192.168.1.140 192.168.1.68 192.168.1.87 192.168.1.79 $
The code for this script is fairly simple. It does all of those one-second ping
s in the background - creating 254 short-lived background processes, so not an ideal tool on an embedded device, but perfectly reasonable on any modern server. If the ping
is successful, it displays the address which was ping
ed.
Download ping1.sh
#!/bin/bash prefix=${1:-192.168.1} for ip in `seq 1 254` do ( ping -c1 -w1 ${prefix}.${ip} > /dev/null 2>&1 && echo ${prefix}.${ip} ) & done wait
It loops through numbers 1 to 254, and uses them as the final octet of the IP address. It defaults to testing the common 192.168.1.0/24 network, but you can pass it any other Class C network address (with three dotted numbers), such as 10.172.19, or 172.17.10, and so on.
The brackets around the command: "( ping ... && echo ... ) &
" mean that the pair of commands are run in a subshell (the &&
means that the echo
only runs if the
ping
succeeded), and the final "&" means that
that subshell is run in the background.
As a result, the whole script runs in a fraction over one second, and each subshell returns a message if it was successful.
The final wait
call ensures that the script waits for all of its children
to complete before exiting. Otherwise, you would get some responses being displayed to the
terminal after the prompt has been displayed to return control to your interactive session.
This would cause messy output, so the wait
simply ensures that all the work has been
done before exiting.
Version 2 - Beyond Class C Subnets
For many purposes, the script above is sufficient. However, not all networks range from .0 to .254. Some
are larger, many today are smaller. Or you may only want to use ping
to test some subset of your network.
The code to flexibly allow you to ping
192.168.1.50 through to 192.168.3.29 (for example) would be
rather messy. This variation on the script uses two functions, making use of the fact that an
IPv4 address is really just one big 32-bit number. So 192.168.1.10 can be represented as:
Decimal | 192 | 168 | 1 | 10 |
---|---|---|---|---|
Binary | 11000000 | 10101000 | 00000001 | 00001010 |
32-bit Int | 3232235786 |
The upshot of this is that ping 192.168.10
is exactly the same as
ping 3232235786
. (It's also why DNS domain names beginning with digits are a bad idea, but that's another story).
This means that we can ping
from the first to the last address simply by incrementing the
integer each time, without having to worry about netmasks, CIDR notation, and so on.
Introducing bitwise shifting
The two functions which achieve this are called dotted_quad_to_integer()
and
integer_to_dotted_quad()
. I have actually provided two slightly different
implementations of dotted_quad_to_integer()
; one uses the external program bc
, which is not always available, and which will slow down the execution time if it has
to be called a lot. However, I find it slightly more readable. The other uses bitwise shifts,
which some people find harder to read, but is more efficient in this case.
An aside: Looking at the table above, we can see that we take 192
as 11000000
, then shift it 24 bits to the left, to put it in the correct place. We then take 168 as 10101000 and shift it 16 bits to the left, so that it fits in next to 192
. Then 1
(00000001
) is shifted 8 bits, and 10
(00001010
) does not need to be shifted, as it's already in place.
Another aside: "dotted quad" means the 4 (quad of) numbers separated by dots, which is the normal way to represent an IP address, such as 192.168.1.10
, as opposed to the "3232235786
" format above.
dotted_quad_to_integer()
dotted_quad_to_integer()
starts by reading the argument into four different variables,
${a}
, ${b}
, ${c}
and ${d}
. By setting the
Internal Field Seperator ($IFS
) to ".
", the string 192.168.1.10 is read as four different variables - no need to call out to an external cut
or
awk
program. This makes the processing far quicker - often a constraint
with shell scripts.
The first implementation then passes these all to the bc
utility to calculate.
The first octet (192, in this case) is multiplied by 256^3, the second (168) by 256^2, the third
by 256^1 (which is just 256) and the third by 256^0, which is always 1. Add them together, and
the answer 3232235786 comes out:
alternate_dotted_quad_to_integer() { IFS="." read a b c d <<< `echo $1` echo "($a * 256 * 256 * 256) + ($b * 256 * 256) + ($c * 256) + $d" | bc } $ alternate_dotted_quad_to_integer 192.168.1.10 3232235786
The other method uses the built-in expr
call to achieve the same result, using
the bitwise shift operator as mentioned above: "<<
":
dotted_quad_to_integer() { IFS="." read a b c d <<< `echo $1` expr $(( (a<<24) + (b<<16) + (c<<8) + d )) } $ dotted_quad_to_integer 192.168.1.10 3232235786
This method is more efficient, and perfectly readable. Both alternatives are presented for comparison.
integer_to_dotted_quad()
Calculating the reverse, for displaying the IP addresses in human-readable format, is less
obvious. This uses a similar technique to the second dotted_quad_to_integer()
implementation, but in reverse. For readability, it defines four variables - again,
${a}
, ${b}
, ${c}
and ${d}
, for the sake of
giving them a name.
integer_to_dotted_quad() { local ip=$1 let a=$((ip>>24&255)) let b=$((ip>>16&255)) let c=$((ip>>8&255)) let d=$((ip&255)) echo "${a}.${b}.${c}.${d}" }
This then simply echo
es out the resulting variables with the customary dots
between the octets.
Main Body of Script
The main body of the script then defines the ${start}
and ${end}
variables to the 32-bit representations of the desired start and end IP addresses, and loops
through them just as the first script did, ping
ing each IP in the background
and displaying the results if it gets a response.
start=$(dotted_quad_to_integer $first_ip) end=$(dotted_quad_to_integer $last_ip) for ip in `seq $start $end` ...Download ping2.sh
#!/bin/bash first_ip=${1:-192.168.1.1} last_ip=${2:-192.168.1.254} alternate_dotted_quad_to_integer() { IFS="." read a b c d <<< `echo $1` echo "($a * 256 * 256 * 256) + ($b * 256 * 256) + ($c * 256) + $d" | bc } dotted_quad_to_integer() { IFS="." read a b c d <<< `echo $1` expr $(( (a<<24) + (b<<16) + (c<<8) + d)) } integer_to_dotted_quad() { local ip=$1 let a=$((ip>>24&255)) let b=$((ip>>16&255)) let c=$((ip>>8&255)) let d=$((ip&255)) echo "${a}.${b}.${c}.${d}" } start=$(dotted_quad_to_integer $first_ip) end=$(dotted_quad_to_integer $last_ip) for ip in `seq $start $end` do ( ping -c1 -w1 ${ip} > /dev/null 2>&1 && integer_to_dotted_quad ${ip} ) & done wait
A sample run of the script, from the top end of one subnet, to the start of another. (Note that results come back out-of-sequence due to the background nature of the pings):
$ ./ping2.sh 66.175.210.250 66.175.211.10 66.175.211.9 66.175.211.3 66.175.211.1 66.175.211.2 66.175.211.6 66.175.211.10 66.175.211.7 66.175.211.5 66.175.210.253 66.175.210.251 66.175.210.250 $
Hopefully you found this useful and interesting, and hopefully there was something new in there which you did not know before. Feedback is always most welcome via email.
You may also find my Shell Script Tutorial useful.
og:image credit: The Story About PING by Marjorie Flack and Kurt Wiese. Some of the reviews are very funny:
While we're mentioning books on Amazon:
Invest in your career. Buy my Shell Scripting Tutorial today: