I debated for a long time whether to include coding in my networking blog. But since it seems the future of networking lies in code and automation, I believe it is time for some code.
Today we’ll look at how we can quickly ping sweep a subnet using python. If you are looking for some resources on learning python, you might check out this free ebook on python for network engineers or getting a course on Udemy for python. If you’d like me to create a python learning resource in the future, please let me know in the comments or contact form!
Topology
We’ll use a simple IP subnet of 172.16.0.0/24 to sweep. Connected devices have been placed randomly at 172.16.0.51, 172.16.0.121 and 172.16.0.253. We’ll write the sweep python code on the Ubuntu 20.04 server at 172.16.0.1. Let’s get started!
Simple python ping sweep script
The basic logic of the code will be to loop through all hosts in the 172.16.0.0/24 subnet, from 172.16.0.1 to 172.16.0.254 (.0 and .255 are the network and broadcast addresses, so no need to ping them) and ping each IP address once. If it responds, we’ll print to the CLI that it worked.
We’ll use two modules, ipaddress
and subprocess
. ipaddress
is a handy network tool for working with IP addresses. Knowing where to start and stop in the loop is relatively simple with a /24 subnet, what if it were 172.16.0.0/19? Just incrementing the fourth octet by 1 each time won’t work. You’ll go to 172.16.0.256, which isn’t a valid IP address. That’s where ipaddress
helps out. subprocess
lets us call the ping
command from python.
Here’s our code:
import ipaddress import subprocess mynet = ipaddress.ip_network('172.16.0.0/24') #create an ipaddress object for 172.16.0.0/24 for host in mynet.hosts(): #loop through each host of 172.16.0.0/24 host = str(host) #change from ipaddress object to string to hand to ping command proc = subprocess.run( #use subprocess to call ping command, split to multiple lines cuz its long ['ping', host, '-c', '1'], #calling ping here, putting in host to ping stderr=subprocess.DEVNULL, #silence ping command errors stdout=subprocess.DEVNULL #silence ping command output ) if proc.returncode == 0: #return code of 0 from ping command means it got a reply print(f'{host} is alive!') #say this host is alive if we got a reply
Hopefully this is pretty straightforward. The magic is happening in a couple spots. The first is with this line:
mynet = ipaddress.ip_network('172.16.0.0/24')
This creates an “object” that holds the network we’re working with. This object has super powers, one of them is visible in this line:
for host in mynet.hosts():
It lets us move through the hosts of the subnet, from 172.16.0.1 to 172.16.0.254, each time the host IP is assigned to host
. We can then hand host
to the ping
command.
The second spot with magic is here:
proc = subprocess.run( ['ping', host, '-c', '1'], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL )
This is just spawning another process, ping
, from python.
When we run the script, we should see all the IP addresses that are alive!
python3 ping_sweep.py #might be python or python3 based on your OS 172.16.0.1 is alive! 172.16.0.51 is alive! 172.16.0.121 is alive! 172.16.0.253 is alive!
There’s only one problem – the script takes quite a while to run. The issue is that each time the ping
command runs, python waits until it finishes before moving to the next. This is known as “blocking”, which basically means the script comes to a halt while it’s waiting for a ping process to finish. A common /24 size subnet of 254 hosts takes a good while to complete.
What if we could ping them all at the same time, or close to it? Well, this leads us into the dark, dangerous world of multi-threading, parallel processing, multiprocessing, and asynchronous processing. Even the words are ominous-sounding. But don’t worry, it’s not so bad with the help of a handy module called asyncio
.
Super-charged ping sweep with asyncio
The issue we’re faced with is that we need to do multiple things at once. There are many ways to solve this problem, and some people spend their whole careers in this complex field. Recently though, the python asyncio
is getting popular because it’s relatively easy to work with and not so terribly complicated compared to others. As of python 3.6, it’s part of the standard library.
Here’s the same ping sweep, this time written using the python asyncio
module:
import ipaddress import asyncio async def ping(host): #add the "async" keyword to make a function asynchronous host = str(host) #turn ip address object to string proc = await asyncio.create_subprocess_shell( #asyncio can smoothly call subprocess for you f'ping {host} -c 1', #ping command stderr=asyncio.subprocess.DEVNULL, #silence ping errors stdout=asyncio.subprocess.DEVNULL #silence ping output ) stdout,stderr = await proc.communicate() #get info from ping process if proc.returncode == 0: #if process code was 0 print(f'{host} is alive!') #say it's alive! loop = asyncio.get_event_loop() #create an async loop tasks = [] #list to hold ping tasks mynet = ipaddress.ip_network('172.16.0.0/24') #ip address module for host in mynet.hosts(): #loop through subnet hosts task = ping(host) #create async task from function we defined above tasks.append(task) #add task to list of tasks tasks = asyncio.gather(*tasks) #some magic to assemble the tasks loop.run_until_complete(tasks) #run all tasks (basically) at once
No denying it, this is more complicated. Might be a bit foreign even if you’re familiar with python. The key thing here is that we define a single ping task in an async function. Then when we loop through the subnet hosts, we create a task from that function, instead of running it on the spot. Then we call the asyncio module at the end to gather up the tasks and run them all asynchronously, which has the effect of appearing to run them all at once.
Also note that there’s no subprocess module here. Asyncio has built-in subprocess management (check the documentation here), so no need for the standard subprocess module.
While the output is the same, you’ll notice this takes about a second to run compared to minutes for the first script:
python3 ping_sweep.py 172.16.0.1 is alive! 172.16.0.51 is alive! 172.16.0.121 is alive! 172.16.0.253 is alive!
Please let me know in the comments if you want to see more content like this!