<<< Back to Tips Index

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 pings 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 pinged. 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:

Decimal192168110
Binary11000000101010000000000100001010
32-bit Int3232235786

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 echoes 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, pinging 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:

 

Steve's Bourne / Bash shell scripting tips
Share on Twitter Share on Facebook Share on LinkedIn Share on Identi.ca Share on StumbleUpon