Since we know that arp-scan is relatively quick, generally it’ll return a result in a much shorter time than nmap. It’s also pretty reliable about finding devices on the network, so we can use it to keep a running count of the number of devices (and which devices) are using our network throughout the day.
The easiest way to do this is to use arp-scan to count the devices periodically throughout the day and log the number to a database. Then we can both report the current number, and also do some later analysis on the data. I’ve put together a quick Perl script to do this, but we will need to install a few tools first before we can use it. So go ahead and install the following packages:
$ sudo apt-get install dnsutils
$ sudo apt-get install libdbd-sqlite3-perl
$ sudo apt-get install libgetopt-long-descriptive-perl
$ sudo apt-get install libdatetime-format-iso8601-perl
and then grab the Perl script from Github and save it onto your Raspberry Pi.
#!/usr/bin/env perl
use strict;
use warnings;
use DBI;
use Getopt::Long;
use DateTime;
my ( %opt );
my $status = GetOptions("network=s" => \$opt{"network"},
"dig" => \$opt{"dig"});
$opt{"network"} = "network" unless defined $opt{"network"};
print "\nSCANNING\n--------\n";
my ($stmt, $sth, $rv);
my $dbfile = "/home/pi/$opt{'network'}.db";
my $dbh = DBI->connect("dbi:SQLite:dbname=$dbfile","","");
my $scan = `arp-scan --retry=8 --ignoredups -I wlan0 --localnet`;
my @lines = split("\n", $scan);
my $csv = undef;
my $count = 0;
foreach my $line (@lines) {
chomp($line);
if ($line =~ /^\s*((?:\d{1,3}\.){3}\d{1,3})\s+((?:[a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2})\s+(\S.*)/) {
my $ip = $1;
my $mac = $2;
my $desc = $3;
print "IP=$ip, MAC=$mac, DESC=$desc";
# Dig for the mDNS name associated with the IP
my $mdns = undef;
if (defined($opt{"dig"})) {
$stmt = qq(CREATE TABLE IF NOT EXISTS mdns(mac TEXT NOT NULL PRIMARY KEY UNIQUE, mdns TEXT););
$sth = $dbh->prepare( $stmt );
$rv = $sth->execute();
$sth->finish();
my $dig = `dig -x ${ip} \@224.0.0.251 -p 5353`;
#print $dig;
my @report = split("\n", $dig);
my $answer = 0;
my $local = undef;
foreach my $entry (@report) {
chomp($entry);
if ( $answer == 1 ) {
$local = $entry;
last;
}
if( $entry eq ";; ANSWER SECTION:") {
$answer = 1;
}
}
if ( defined $local ) {
#print "local name = $local\n";
( $mdns ) = ($local =~ /\s+(\S+\.local)\.$/);
print ", LOCAL=$mdns\n";
$stmt = qq(INSERT OR REPLACE INTO mdns (mac, mdns) VALUES ("$mac","$mdns"));
$sth = $dbh->prepare( $stmt );
$rv = $sth->execute();
$sth->finish();
} else {
print "\n";
}
} else {
print "\n";
}
$stmt = qq(CREATE TABLE IF NOT EXISTS macs(mac TEXT NOT NULL PRIMARY KEY UNIQUE, count INTEGER, description TEXT););
$sth = $dbh->prepare( $stmt );
$rv = $sth->execute();
$sth->finish();
$stmt = qq(SELECT count FROM macs WHERE mac="$mac";);
$sth = $dbh->prepare( $stmt );
$rv = $sth->execute();
my @row = $sth->fetchrow_array();
$sth->finish();
my $previous;
if (defined( $row[0] )) {
$previous = $row[0]
} else {
$previous = 0;
}
print "Previously seen '$previous' times\n";
$stmt = qq(INSERT OR REPLACE INTO macs (mac, count, description) VALUES ("$mac",$previous+1,"$desc"));
$sth = $dbh->prepare( $stmt );
$rv = $sth->execute();
$sth->finish();
if (!defined $csv) {
$csv = "$mac";
} else {
$csv = $csv . ",$mac";
}
$count++;
}
}
my $time = DateTime->now->iso8601;
print "\nRESULT\n------\n";
print "time = $time\n";
print "count = $count\n";
print "csv = $csv\n";
$stmt = qq(CREATE TABLE IF NOT EXISTS scan(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, datetime TEXT UNIQUE, count INTEGER, macs TEXT););
$sth = $dbh->prepare( $stmt );
$rv = $sth->execute();
$sth->finish();
$stmt = qq(INSERT INTO scan (datetime, count, macs) VALUES ("$time",$count,"$csv"));
$sth = $dbh->prepare( $stmt );
$rv = $sth->execute();
$sth->finish();
$stmt = qq(CREATE TABLE IF NOT EXISTS days(date TEXT UNIQUE NOT NULL PRIMARY KEY, average INTEGER, samples TEXT););
$sth = $dbh->prepare( $stmt );
$rv = $sth->execute();
$sth->finish();
print "\nAVERAGE\n-------\n";
my $day = DateTime->now->ymd('-');
print "Today is $day\n";
$stmt = qq(SELECT samples FROM days WHERE date="$day";);
$sth = $dbh->prepare( $stmt );
$rv = $sth->execute();
my @row = $sth->fetchrow_array();
$sth->finish();
my $samples;
if (defined( $row[0] )) {
$samples = $row[0];
} else {
$samples = undef;
}
my @values;
if (!defined $samples) {
$values[0] = $count;
$samples = "$count";
} else {
@values = split(',', $samples);
push (@values, $count);
$samples = $samples . ",$count";
}
print "Samples = $samples\n";
#print "values = @values\n";
my $average = 0;
foreach my $value (@values) {
$average = $average + $value;
}
$average = int($average/scalar(@values));
$stmt = qq(INSERT OR REPLACE INTO days (date, average, samples) VALUES ("$day",$average,"$samples"));
$sth = $dbh->prepare( $stmt );
$rv = $sth->execute();
$sth->finish();
print "Current running average for today is $average devices on the network.\n";
$dbh->disconnect();
exit;
The script will perform an ARP scan of the local network on wlan0, and save the results into a SQLite database. The default name for the database is network.db, but this can be modified by passing a database name on the command line with the argument “–network NAME”, where NAME is the name of the database file to which the script will automatically append a ‘.db’ ending.
Optionally the script will look to see if the device offers an mDNS associated forward address. You can enable this by passing the command line argument “–dig”. However this will severely impact the performance of the script and make it slow down a lot. However, since we’re serializing the results into an SQLite database, you only really need to run this script every so often to populate the forward addresses for hosts.
The database consists of four tables. The first table, named scan, records the time and hosts present for each ARP scan.
CREATE TABLE scan(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, datetime TEXT UNIQUE, count INTEGER, macs TEXT)
The next, named macs, records the number of times each unique MAC address has appeared in an ARP scan, along with the vendor name of the NIC if known.
CREATE TABLE macs(mac TEXT NOT NULL PRIMARY KEY UNIQUE, count INTEGER, description TEXT)
The third table, named days, records the number hosts present for each scan on an individual day, as well as a calculated ‘average number of devices connected’ to the network for that day.
CREATE TABLE days(date TEXT UNIQUE NOT NULL PRIMARY KEY, average INTEGER, samples TEXT)
The final table, named mdns, is optionally created when the script is executed with the –dig command line argument. This table stores the mapping between MAC address and mDNS forward address if the device advertises one.
CREATE TABLE mdns(mac TEXT NOT NULL PRIMARY KEY UNIQUE, mdns TEXT)
We can now run this script from crontab regularly, perhaps every half hour or so, and also once or twice a day with the optional (and much slower) “–dig” command line argument to populate the “mdns” table which maps the device’s MAC address to mDNS forward address.
But before we do that, let’s test it out and run the script from the command line
$ sudo ./counter.pl --network home
Or, if you’ve got the patience, you can also look up mDNS forward addresses for the hosts:
$ sudo ./counter.pl --network home --dig
This will create a database called “home.db”. After running the script a few times, go ahead and take a look at the database in your favourite database inspector application.
You’ll notice that the total number of hosts visible does vary a bit, as sometimes the ARP scan misses a host or two, or more. You can make the script more reliable by upping the retries “–retry=8” to a higher number. However, the higher this number, the slower the ARP scan.