Author - Sivakumar RR
From all the discussions I had with different personalities in the industry, one fun fact which I have noticed is that everyone will eventually have a set of tools or tech stacks which they like most or enjoy working with, later in their careers. In my opinion it’s also important to not to keep any reservations and to maintain an open mind which is ready to accept the changes and new opportunities.
Best way to learn something is by trying to implement something interesting and funny. In this article we are going to implement a network scanner for connected devices using Ruby programming language. There is always a better way to implement any solution, so this is just one aspect of the implementation.
Problem Statement : Network scanner application for identifying devices connected to it.
Please Note : There are libraries available for this like “nmap” and all but this is just a plain way of implementation to get into the basics of how things works.
Logical approach for solution,
- Identify the network interfaces available to the network and pick one to proceed
- Gather IP associated and calculate the IP range of the network
- Run an ARP polling to the network range for response
We will be going to use a few gems for easy implementation purposes, first we can use the feature rich library called Socket, it provides access to the underlying operating system socket implementations.
require 'socket'
We can use the Socket gem’s “getifaddrs” method which returns an array of interface addresses. This will be able to help us identify the interface which we are going to use for scanning. A machine can have multiple interfaces, which can be hardware or virtual interfaces as well. In this case we are avoiding interfaces which are not having IPV4 addresses and those which are not having a broadcast address. Idea here is to avoid the type of interfaces like bridge, VPN gateway etc. So let’s add some filters to the method to get this done.
def get_interfaces
addr_infos = Socket.getifaddrs
int_to_use = []
addr_infos.each do |addr_info|
if addr_info.addr && addr_info.broadaddr
if addr_info.addr.ipv4? && addr_info.addr.ip_address !~ /\A127/
int_to_use << addr_info
end
end
end;nil
return int_to_use
end
You have noticed one more condition added extra with a regex filter, that is to avoid the “localhost” or loopback address. So all sets have all the interfaces we can use in the array “int_to_use”.
Now we can have a prompt added in the code to have the interface selection to be done by the user. Which will be helpful in such cases where you want to try multiple interfaces with this script. I will update this on the final script, for now we will be assuming that we are going with an interface named “en0”.
As a next stage we should gather the IP address which is associated with the interface we choose (in this case “en0”). To do this we can easily use our shortlisted interface address object from the array.
Now we need to gather the IP range in that interface, to do this we need to know the IP associated with the same as well. Let’s use the ruby library “ipaddress” here for an easy process.
require 'ipaddress'
#let's assume that the selected interface is stored in"sel_intf" variable
# sel_intf.addr.ip_address ==> can print the IP address of the interface
ips = IPAddress::IPv4.parse_classful(sel_intf.addr.ip_address) #will return the ip range
So now we are ready with interface selection and IP, IP range gathering, now it's time to find out whether any devices are attached with these IPs. To do this we can try by polling all the IPs in the IP range with ARP requests. Why ARP request? Well, ARP or Address Resolution Protocol works in a way that it will send a broadcast message asking for “Who has x.x.x.x IP?” then the device which receives the request which has the IP will respond with “I have the IP and here is the MAC address associated with that”. Please refer to the images below for better understanding.
There are various options to create ARP requests, now we are going with the normal “ping” command and we will be going to parse the “arp” response cache for the details of the device.
r_data = []
`ping #{ip.to_s} -c 1`
arp_res = `arp -a`
arp_res_lines = arp_res.gsub(/\r\n?/, "\n")
arp_res_lines.each_line do |line|
if !line.include?("incomplete") && line.include?(ip.to_s)
r_data << {
:ip => ip.to_s,
:mac => line.split('at')[1].split('on')[0].delete(' '),
:vendor => ''
}
end
end
In the above code block we are running command line codes with “`” (backslash), ruby allows us to perform the same. To get the ARP cache we are using “arp -a” command and parsing that response to identify the lines which are having completed requests and mac addresses. If we run this block in a loop of IPs, at the end of the process, we will be able to get an array of hashes which has an IP and MAC address.
Finally we are ready with our results of available devices in the network, to conclude this, let’s add some bonus blocks into this. Let’s try to get some vendor information for these MAC addresses, this can be done using various options like using other libraries and even to parse the IEEE list of published MAC vendors list. In this article we are going to try an API.
require 'net/http'
def get_vendor_info(r_data)
r_data.each do |r|
vendor = Net::HTTP.get('api.macvendors.com', "/#{r[:mac]}/") rescue nil
if !vendor.include?("errors")
r[:vendor] = vendor
end
end
return r_data
end
This block can add the vendor information by querying at “api.macvendors.com” for MAC address references. At the end we can display the results using the “terminal-table” library in the command line itself. You can get the full code in the Github url mentioned below, the final result will be displayed similar to the image below.
Hope this article helps, happy coding.
You can get the full code on here : https://github.com/sivakumar090/scan_buddy/
I always check this type of advisory post and I found your article which is related to my interest.Houston it support This is a great way to increase knowledge for us. Thanks for sharing an article like this.
ReplyDelete