Photo by Hep Svadja

We’re going to make use of a Raspberry Pi to build a network scanner to keep track of the hosts connecting to our local network. It’s actually pretty easy to do. And as we’re going to make use of ARP scans to do it, which are moderately stealthy unless you heavily hammer the network, it would be easy to make something that is pretty much invisible to most people.

If you did that, you’d end up with a Pi that could sit quietly in a corner and monitor your home or office wireless network, squirreling away information about what hosts are connecting to without anyone taking much notice of it at all. However, we’re not those sort of people. We’re more the glowing red 6-inch tall seven-segment display sort of people. So instead, our network scanner is going to be plenty visible.

Follow along with our live build while you make your own:

YouTube player

Buying Hardware

Most of the hardware for this project is fairly easy to get a hold of and, apart from the giant seven-segment displays and the controller boards for them, you may well have most (or even all) of the bits and pieces you need kicking around your workshop already.

It can also be put together with minimal soldering. The only soldering needed for the project is attaching the controller boards to the seven-segment displays. These boards use castellated mounting holes, which can be tricky to solder if you’re more used to through hole components. However, don’t worry as it’s not anywhere near as hard as SMD soldering and there are several good guides to help you out.

Optionally Adding a Second Wireless Adaptor

We’re going to be adding a second USB WiFi dongle to our Raspberry Pi since the BCM43438 WiFi chipset on the Raspberry Pi 3 does not currently support monitoring mode.

This second adaptor isn’t necessary for our network scanner, as we’ll be using the onboard WiFi adaptor for the most part, but it’ll give us a lot more flexibility later on if we want to do more in-depth monitoring of our network environment. A USB wireless adaptor is also cheap and readily available, so there isn’t a lot of reasons not to add it unless you’re on a really tight budget.

There are really only two or three chipsets used inside mass market USB wireless adaptors. Unfortunately, you’ll need to be somewhat careful about picking out a wireless adaptor that does support monitoring mode as even seemingly identical adaptors, at least from the outside, may be using completely different chipsets on the inside. However, the adapters are cheap, so if you make a mistake, it’s pretty easy just to buy another. Having another USB wireless dongle in your spares box is always handy.

Some of these chipsets are harder to get working in monitor mode than others. For instance, while the Realtek RTL8188CUS chip can support monitoring, and works out of the box on Raspbian, the default Raspbian drivers don’t support monitoring mode. Getting it working is rather tedious.

You can easily check the chipset of a USB wireless adaptor on most Linux computers by typing

 $ lsusb
 Bus 001 Device 004: ID 148f:5370 Ralink Technology, Corp. RT5370 Wireless Adapter
 Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
 Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp.
 Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

In this case you can see that the USB wireless dongle we are using has a Ralink Technology RT5370 chipset. This supports monitoring out of the box and additionally it has the all-too-unusual ability to support both monitoring and promiscuous modes, so you’re able to see unicast, multicast, and broadcast frames. If you can manage to pick a USB adaptor up using this chipset, you’re in the clear.

Project Steps

Getting an OS for the Raspberry Pi

The first thing we need to do is get the Raspberry Pi set up and ready. Go ahead and download the latest version of Raspbian Lite. Insert an SD card into your Macbook, open up a Terminal window, and type df -h, remember the device name for your SD Card. In my case it’s /dev/disk1. We’ll need to use the raw device, /dev/rdisk1.

 $ df -h
 Filesystem Size Used Avail Capacity iused ifree %iused Mounted on
 /dev/disk0s2 699Gi 367Gi 332Gi 53% 96214802 86992771 53% /
 devfs 206Ki 206Ki 0Bi 100% 714 0 100% /dev
 map -hosts 0Bi 0Bi 0Bi 100% 0 0 100% /net
 map auto_home 0Bi 0Bi 0Bi 100% 0 0 100% /home
 /dev/disk1s1 59Gi 33Gi 26Gi 57% 8739054 6768902 56% /Volumes/SD Card

Unmount the card

 $ sudo diskutil unmount /dev/disk1s1

rather than ejecting it by dragging it to the trash. Then in the Terminal window change to the directory with your downloaded disk image and type

 $ unzip 2017-01-11-raspbian-jessie-lite.zip
 $ sudo dd bs=1m if=2017-01-11-raspbian-jessie-lite.img of=/dev/rdisk1

if the above command reports error “dd: bs: illegal numeric value”, change bs=1m to bs=1M. The card should automatically remount the image’s boot partition, when dd is done. Since recent releases of the Raspbian operating system have disabled SSH on first boot, and we’re intending to run the board without a monitor or keyboard, we’ll have to enable it again.

Navigate to the boot partition and type the following

 $ cd /Volumes/boot
 $ touch ssh
 $ cd ~

The contents of the ssh file don’t matter. When the Pi first boots, it looks for this file; if it finds it, it will enable SSH and then delete the file.

Eject the card with the command

 $ sudo diskutil eject /dev/rdisk1

Alternatively, if you don’t want to use dd, I recommend Etcher as a good app to burn SD Card images on the Mac.

Booting the Raspberry Pi

Insert the SD card in the board, plug in your WiFi dongle, and, for now, plug the board into the Ethernet. Then connect the power to start it booting. A red LED will initially come on, however the green ACT LED should start flashing, and the orange FDX and LNK LEDS should light up if the boot works correctly.

After booting, the Raspberry Pi should advertise itself using mDNS, with the default name of raspberrypi.local. The easiest way to find it will therefore be to see if it responds to a ping

 $ ping raspberrypi.local
 PING raspberrypi.local (192.168.1.159): 56 data bytes
 64 bytes from 192.168.1.159: icmp_seq=0 ttl=64 time=4.079 ms
 64 bytes from 192.168.1.159: icmp_seq=1 ttl=64 time=4.223 ms
 64 bytes from 192.168.1.159: icmp_seq=2 ttl=64 time=6.717 ms
 ^C
 --- raspberrypi.local ping statistics ---
 3 packets transmitted, 3 packets received, 0.0% packet loss
 round-trip min/avg/max/stddev = 4.079/5.006/6.717/1.211 ms

If the Raspberry Pi doesn’t respond to a ping request the next easiest thing to do will be to log on to your network router and search for the IP address it was allocated using DHCP.

Configuring the Raspberry Pi

Once you find the Pi, go ahead and login with ssh. The default username and password are pi and raspberry respectively, and go ahead and configure it ready for use. Type

 % sudo raspi-config

at the prompt to start up the Raspbian configuration utility.

This will open the configuration manager. The first option down is Expand Filesystem, which will automagically expand the size of the root partition. Next scroll down to Advanced Options and change the hostname to something less generic. I went with netscan.

Also under Advanced Options are the toggles to enable SSH, SPI, I2C, and Serial. Go ahead and enable all of these now. While you have the configuration utility open, remember to change the user password to something a bit more secure. Finally, hit Finish, and allow the Raspberry Pi to reboot.

Once you login, you can update the Raspberry Pi to the latest version. Type

 $ sudo apt-get update
 $ sudo apt-get upgrade

to update to the latest bug fixed versions of installed packages. If you’re using an older version of Raspbian you may also want to type

 $ sudo apt-get dist-upgrade

to upgrade to the latest version of the operating system.

About Our Wireless Networking

Essentially we’re going to put the Raspberry Pi’s on-board wireless adaptor (wlan0) into promiscuous mode. This allows us to capture packets on a network to which we’re connected. While we’ll use the external USB wireless adaptor (wlan1) in monitor mode, this captures packets regardless of connected network. No association to an Access Point needed (and no authentication).

Let’s start by checking the current network configuration. At the prompt type

 $ ifconfig
 eth0 Link encap:Ethernet HWaddr b8:27:eb:97:cc:e4
 inet addr:192.168.1.159 Bcast:192.168.1.255 Mask:255.255.255.0
 inet6 addr: fe80::b059:6454:1d28:763a/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
 RX packets:88128 errors:0 dropped:32 overruns:0 frame:0
 TX packets:45378 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:122233058 (116.5 MiB) TX bytes:3828475 (3.6 MiB)
 
 lo Link encap:Local Loopback
 inet addr:127.0.0.1 Mask:255.0.0.0
 inet6 addr: ::1/128 Scope:Host
 UP LOOPBACK RUNNING MTU:65536 Metric:1
 RX packets:38581 errors:0 dropped:0 overruns:0 frame:0
 TX packets:38581 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1
 RX bytes:3395128 (3.2 MiB) TX bytes:3395128 (3.2 MiB)

 wlan0 Link encap:Ethernet HWaddr b8:27:eb:c2:99:b1
 inet6 addr: fe80::9380:71d4:4917:9b65/64 Scope:Link
 UP BROADCAST MULTICAST MTU:1500 Metric:1
 RX packets:7 errors:0 dropped:7 overruns:0 frame:0
 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:773 (773.0 B) TX bytes:0 (0.0 B)

 wlan1 Link encap:Ethernet HWaddr 00:0f:60:05:a8:5c
 UP BROADCAST MULTICAST MTU:1500 Metric:1
 RX packets:0 errors:0 dropped:0 overruns:0 frame:0
 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

Here you see that loopback network device (lo), the ethernet network (eth0) which we’re currently using to connect to the Pi and its IP address, the on-board Raspberry Pi wireless adaptor (wlan0), and finally our USB WiFi dongle (wlan1). You can see that both wlan0 and wlan1 aren’t configured yet. But we’ll get to that in a minute.

We can take a closer look at the wireless adaptors:

 $ iwconfig
 wlan0 IEEE 802.11bgn ESSID:off/any
 Mode:Managed Access Point: Not-Associated Tx-Power=31 dBm
 Retry short limit:7 RTS thr:off Fragment thr:off
 Power Management:on

 lo no wireless extensions.

 eth0 no wireless extensions.

 wlan1 IEEE 802.11bgn ESSID:off/any
 Mode:Managed Access Point: Not-Associated Tx-Power=20 dBm
 Retry short limit:7 RTS thr:off Fragment thr:off
 Power Management:off

 $ iw dev
 phy#1
 Interface wlan1
 ifindex 4
 wdev 0x100000001
 addr 00:0f:60:05:a8:5c
 type managed
 phy#0
 Interface wlan0
 ifindex 3
 wdev 0x1
 addr b8:27:eb:c2:99:b1
 type managed

We’ll be configuring the Raspberry Pi’s own wireless adaptor (wlan0) to put the Pi onto our network, while reserving our USB dongle (wlan1) for monitoring. Just to be safe, double check that the USB adaptor is capable of being configured for monitoring:

 $ sudo iw phy phy1 interface add mon1 type monitor
 $ iw dev
 phy#1
 Interface mon1
 ifindex 5
 wdev 0x100000002
 addr 00:0f:60:05:a8:5c
 type monitor
 Interface wlan1
 ifindex 4
 wdev 0x100000001
 addr 00:0f:60:05:a8:5c
 type managed
 phy#0
 Interface wlan0
 ifindex 3
 wdev 0x1
 addr b8:27:eb:c2:99:b1
 type managed

If you see the mon1 interface appear, everything is working as expected, and for now you can tear down the interface again with

 $ sudo iw dev mon1 del

If you don’t, check that the chip set really supports monitoring. Type

 $ iw phy phy1 info

Somewhere in the output you should see something a lot like this:

 Supported interface modes:
 * IBSS
 * managed
 * AP
 * AP/VLAN
 * WDS
 * monitor
 * mesh point

with monitor being listed as a supported interface mode. If that’s not the case, either your chipset just doesn’t support monitoring mode or there is a problem with the kernel driver. There are some cases where the mainline kernel driver for the chip set supports monitoring, whilst the default Raspbian driver does not.

Configuring the Wireless Adaptors

Let’s go ahead and put our Raspberry Pi onto our local network using the internal wireless adaptor. First of all, we need to find our network. Go ahead and type

 $ sudo iwlist wlan0 scan

This will perform a scan for networks. Depending on where you are, you may find only a few. If you’re unsure what sort of encryption your network is using, look for a line that looks like this:

 IE: IEEE 802.11i/WPA2 Version 1

Once you have a network SSID and encryption method, we can go ahead and put our Raspberry Pi onto the network. On the assumption you’re using WPA2, open the /etc/wpa_supplicant/wpa-supplicant file in your editor of choice:

 $ sudo nano /etc/wpa_supplicant/wpa_supplicant.conf

Go to the bottom of the file and add the following:

 network={
 ssid="SSID"
 psk="PASSWORD"
 }

SSID is the ESSID of your home network, and PASSWORD is the WPA2 password for your network. As a side note, if you want to configure two (or more) wireless networks you can do so by adding an id_str to each like this:

 network={
 ssid="OFFICE"
 psk="OFFICE-PASSWORD"
 id_str="office"
 }

 network={
 ssid="HOME"
 psk="HOME-PASSWORD"
 id_str="home"
 }

When booted, the Raspberry Pi should associate with either network. Additionally, if both networks are present you can add a priority key, and the network with the highest priority will be used first.

After saving the configuration file, wpa-supplicant should notice a configuration change has been made and, within a few seconds, should try and connect to your (priority) wireless network. Generally however, it doesn’t, and you should type the following:

 $ sudo ipdown wlan0
 $ sudo ifup wlan0

Wait a few seconds and then:

 $ ifconfig wlan0
 wlan0 Link encap:Ethernet HWaddr b8:27:eb:c2:99:b1
 inet addr:192.168.1.217 Bcast:192.168.1.255 Mask:255.255.255.0
 inet6 addr: fe80::9380:71d4:4917:9b65/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
 RX packets:441 errors:0 dropped:416 overruns:0 frame:0
 TX packets:20 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:126262 (123.3 KiB) TX bytes:3634 (3.5 KiB)

The interface should have acquired an IP address. Now we need to configure our USB wireless adaptor. Here we’re going to do something slightly different. Go ahead and open the /etc/network/interfaces configuration file in your favourite editor, and change the wlan1 entry to

 allow-hotplug wlan1
 iface wlan1 inet manual
 pre-up iw phy phy1 interface add mon1 type monitor
 pre-up iw dev wlan1 del
 pre-up ifconfig mon1 up

This should tear down the managed interface and put the wireless adaptor into monitor mode on boot. Once you’ve made these changes, go ahead and reboot your Pi with

 $ sudo reboot

Once the Raspberry Pi has rebooted, log back in and check the networking configuration. You should see something a lot like

 $ ifconfig
 eth0 Link encap:Ethernet HWaddr b8:27:eb:97:cc:e4
 inet addr:192.168.1.159 Bcast:192.168.1.255 Mask:255.255.255.0
 inet6 addr: fe80::b059:6454:1d28:763a/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
 RX packets:110 errors:0 dropped:0 overruns:0 frame:0
 TX packets:95 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:17019 (16.6 KiB) TX bytes:13954 (13.6 KiB)

 lo Link encap:Local Loopback
 inet addr:127.0.0.1 Mask:255.0.0.0
 inet6 addr: ::1/128 Scope:Host
 UP LOOPBACK RUNNING MTU:65536 Metric:1
 RX packets:0 errors:0 dropped:0 overruns:0 frame:0
 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1
 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

 mon1 Link encap:UNSPEC HWaddr 00-0F-60-05-A8-5C-30-30-00-00-00-00-00-00-00-00
 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
 RX packets:1414 errors:0 dropped:1414 overruns:0 frame:0
 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:335469 (327.6 KiB) TX bytes:0 (0.0 B)

 wlan0 Link encap:Ethernet HWaddr b8:27:eb:c2:99:b1
 inet addr:192.168.1.217 Bcast:192.168.1.255 Mask:255.255.255.0
 inet6 addr: fe80::9380:71d4:4917:9b65/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
 RX packets:134 errors:0 dropped:77 overruns:0 frame:0
 TX packets:31 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:30147 (29.4 KiB) TX bytes:5258 (5.1 KiB)"

and checking the devices we should see something like

 $ iw dev
 phy#1
 Interface mon1
 ifindex 5
 wdev 0x100000002
 addr 00:0f:60:05:a8:5c
 type monitor
 channel 1 (2412 MHz), width: 20 MHz (no HT), center1: 2412 MHz
 phy#0
 Interface wlan0
 ifindex 3
 wdev 0x1
 addr b8:27:eb:c2:99:b1
 ssid OFFICE
 type managed

With wlan0 connected to our home network and wlan1 in monitoring mode, by default, on wireless Channel 1.

While we don’t need to do this right now, we can change the channel, either by specifying the mid-frequency in MHGz, or the channel number. So either

 $ sudo iw dev mon1 set freq 2437

or

 $ sudo iwconfig mon1 channel 6
 $ iw dev
 phy#1
 Interface mon1
 ifindex 5
 wdev 0x100000002
 addr 00:0f:60:05:a8:5c
 type monitor
 channel 6 (2437 MHz), width: 20 MHz (no HT), center1: 2437 MHz
 phy#0
 Interface wlan0
 ifindex 3
 wdev 0x1
 addr b8:27:eb:c2:99:b1
 ssid OFFICE
 type managed

will switch our USB wireless adaptor from Channel 1 to Channel 6. Remembering that

 $ sudo iwlist wlan0 scan

will give a list of nearby wireless networks, including detailing the channel those networks are using. This means we’re now all set up. We can connect the Raspberry Pi to a known network (or networks) on wlan0, which can be put into promiscuous mode to monitor traffic. We can also use mon1 interface to monitor traffic on another wireless network (or by scanning between frequencies, multiple networks).

If you ever need to (temporarily) return mon1 to managed mode, you can do so as follows:

 $ sudo iw dev mon1 del
 $ sudo iw phy phy1 interface add wlan1 type managed

We’ve finished configuring our wireless adaptors, so it’s safe to unplug your ethernet cable at this point. You won’t be needing it any more unless you want to access the Raspberry Pi from the wired network.

Monitoring Tools

Kismet is a wireless network detector, sniffer, and intrusion detection system that differs from other wireless network detectors by working passively. In other words, it detects without sending any loggable packets. It is able to detect the presence of both wireless access points and wireless clients, and associate them with each other. It is the most widely used and up to date open source wireless monitoring tool.

To get started, let’s go ahead and download, build, and install kismet.

 $ sudo apt-get install git-core build-essential
 $ sudo apt-get install libncurses5-dev libpcap-dev libpcre3-dev libnl-dev libmicrohttpd10 libmicrohttpd-dev
 $ wget http://kismetwireless.net/code/kismet-2016-07-R1.tar.xz
 $ tar -xvf kismet-2016-07-R1.tar.xz
 $ cd kismet-2016-07-R1
 $ ./configure
 $ make
 $ sudo make suidinstall
 $ sudo usermod -a -G kismet pi
 $ sudo mkdir -p /usr/local/lib/kismet/
 $ mkdir -p /home/pi/.kismet/plugins/
 $ sudo mkdir -p /usr/lib/kismet/
 $ sudo reboot

Once the Raspberry Pi has rebooted, we need to make a couple of quick changes to the kismet configuration file. Go ahead and open /usr/local/etc/kismet.conf in your favourite editor and modify the following two entries as below,

 ncsource=mon1
 hidedata=true

Next we need to download the manufacturer list. This is useful to identify the Wireless Interface Manufacturer of the various networked devices that kismet will encounter.

 $ sudo mkdir -p /usr/share/wireshark/
 $ cd /usr/share/wireshark/
 $ sudo wget -O manuf http://anonsvn.wireshark.org/wireshark/trunk/manuf
 $ sudo cp manuf /etc/manuf

Now we’ve configured kismet we can start the server component directly from the command line

 $ kismet_server

or by starting it as a daemon

 $ kismet_server -c mon1 --daemonize

We can then start the kismet_client from the command line

 $ kismet_client

which will bring up the default kismet text client.

Once we’ve verified that the kismer_server is running okay by connecting to it from the client, we can launch it on boot by starting the server from /etc/rc.local on boot.

However before we do that, we need to enable the rc.local service itself, and to do so we need to create an rc-local.service file

 $ sudo vi /etc/systemd/system/rc-local.service

the contents of which should look like this:

 [Unit]
 Description=/etc/rc.local Compatibility
 ConditionPathExists=/etc/rc.local

 [Service]
 Type=forking
 ExecStart=/etc/rc.local start
 TimeoutSec=0
 StandardOutput=tty
 RemainAfterExit=yes
 SysVStartPriority=99

 [Install]
 WantedBy=multi-user.target

Save and close the file. Make sure /etc/rc.local file is executable.

 $ sudo chmod +x /etc/rc.local

and enable the service on system boot:

 $ sudo systemctl enable rc-local
 $ sudo systemctl status -l rc-local.service
 ● rc-local.service - /etc/rc.local Compatibility
 Loaded: loaded (/etc/systemd/system/rc-local.service; enabled)
 Drop-In: /etc/systemd/system/rc-local.service.d
 └─ttyoutput.conf
 Active: active (exited) since Thu 2017-02-02 12:08:33 UTC; 1min 56s ago

 Feb 02 12:08:33 netscan systemd[1]: Starting /etc/rc.local Compatibility...
 Feb 02 12:08:33 netscan systemd[1]: Started /etc/rc.local Compatibility.

If all goes well, we can now edit the default /etc/rc.local to start our kismet_server on boot.

 #!/bin/sh -e

 su pi -c '/usr/local/bin/kismet_server -n -c mon1 --daemonize'
 exit 0

Now every time we boot the Pi, the Kismet server will be started in the background. Note the use of the ‘-n’ command line option to suppress logging so we don’t fill up our SD Card with logging information.

You can access the server locally on the Pi just by running kismet_client as normal. Or over the network at netscan.local:2501.

Scanning Tools

Now we’ve got some basic monitoring in place, or possibly some fancier hacks, let’s get to the meat of our network scanner project, and install some tools to find and count the devices attached to our home or office network. The first thing we need to install is nmap., and then arp-scan.

 $ sudo apt-get install nmap
 $ sudo apt-get install arp-scan

While you might be familiar with nmap, you may not be so familiar with arp-scan. It’s a very fast ARP packet scanner that will show every active device on your local subnet.

The interesting thing about this type of tool is, even if a device doesn’t respond to network requests and is designed to be silent, it should be visible to an ARP scan. The downside is, since ARP is non-routable, this type of scanner only works on the local network segment. If you have a complicated network, with multiple routers, it’s unlikely to catch all the hosts on the network.

Running arp-scan on my local network gives a result that looks something like this:

 $ sudo arp-scan --retry=8 --ignoredups -I wlan0 --localnet
 Interface: wlan0, datalink type: EN10MB (Ethernet)
 Starting arp-scan 1.8.1 with 256 hosts (http://www.nta-monitor.com/tools/arp-scan/)
 192.168.1.93 70:73:cb:b2:91:ee (Unknown)
 192.168.1.130 70:73:cb:b2:91:ee (Unknown)
 192.168.1.91 e0:cb:ee:41:ce:29 (Unknown)
 192.168.1.208 70:73:cb:b2:91:ee (Unknown)
 192.168.1.253 9e:97:26:94:76:e4 (Unknown)
 192.168.1.254 9c:97:26:94:76:e4 (Unknown)
 192.168.1.131 f4:5c:89:8b:79:a7 (Unknown)
 192.168.1.120 e0:ac:cb:a3:1d:04 (Unknown)
 192.168.1.121 60:03:08:aa:8c:0a (Unknown)
 192.168.1.129 14:10:9f:d2:23:57 (Unknown)
 192.168.1.126 e4:f8:9c:91:14:73 (Unknown)
 192.168.1.214 00:26:ab:61:a3:b3 SEIKO EPSON CORPORATION
 192.168.1.127 28:e1:4c:9c:89:4f (Unknown)
 192.168.1.125 f4:e3:fb:dc:19:08 (Unknown)
 192.168.1.132 60:92:17:71:f4:6d (Unknown)

 24 packets received by filter, 0 packets dropped by kernel
 Ending arp-scan 1.8.1: 256 hosts scanned in 7.327 seconds (34.94 hosts/sec). 15 responded

You can get a slightly more helpful version of the same scan by replacing the mac-vendor.txt file in /usr/share/arp-scan with a more comprehensive version. Now, as it happens, Wireshark comes with a rather good list of NIC vendor codes. Unfortunately, the format of the file isn’t compatible with arp-scan, at least not out of the box. However, after massaging the file with half a dozen regular expressions, you can massage the data into something that arp-scan will accept.Fortunately for you, I’ve done the heavy lifting for you and posted it as a Gist.

You can therefore go ahead and update the mac-vendor.txt file as follows:

 $ cd /usr/share/arp-scan
 $ sudo mv mac-vendor.txt mac-vendor.orig
 $ sudo wget http://bit.ly/mac-vendor
 $ sudo mv mac-vendor mac-vendor.txt

Now the same network scan produces something a bit more interesting:

 $ sudo arp-scan --retry=8 --ignoredups -I wlan0 --localnet
 Interface: wlan0, datalink type: EN10MB (Ethernet)
 Starting arp-scan 1.8.1 with 256 hosts (http://www.nta-monitor.com/tools/arp-scan/)
 192.168.1.208 70:73:cb:b2:91:ee Apple, Inc.
 192.168.1.86 00:e0:4c:c6:86:01 REALTEK SEMICONDUCTOR CORP.
 192.168.1.253 9e:97:26:94:76:e4 (Unknown)
 192.168.1.254 9c:97:26:94:76:e4 Technicolor
 192.168.1.121 60:03:08:aa:8c:0a Apple, Inc.
 192.168.1.93 28:cf:e9:57:cd:01 Apple, Inc.
 192.168.1.125 f4:e3:fb:dc:19:08 HUAWEI TECHNOLOGIES CO.,LTD
 192.168.1.133 24:4b:81:3d:e9:ad Samsung Electronics Co.,Ltd
 192.168.1.137 ac:e0:10:c7:c5:67 Liteon Technology Corporation
 192.168.1.130 7c:c3:a1:b1:42:52 Apple, Inc.
 192.168.1.129 14:10:9f:d2:23:57 Apple, Inc.
 192.168.1.131 f4:5c:89:8b:79:a7 Apple, Inc.
 192.168.1.126 e4:f8:9c:91:14:73 Intel Corporate
 192.168.1.135 08:6d:41:bf:ff:1a Apple, Inc.
 192.168.1.134 b4:8b:19:2c:5f:de Apple, Inc.
 192.168.1.214 00:26:ab:61:a3:b3 SEIKO EPSON CORPORATION
 192.168.1.120 e0:ac:cb:a3:1d:04 Apple, Inc.
 192.168.1.127 28:e1:4c:9c:89:4f Apple, Inc.
 192.168.1.132 60:92:17:71:f4:6d Apple, Inc.

 33 packets received by filter, 0 packets dropped by kernel
 Ending arp-scan 1.8.1: 256 hosts scanned in 7.927 seconds (32.29 hosts/sec). 19 responded

Counting Devices

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.

Adding to Crontab

Now we have everything working, let’s go ahead and add our crontab file to the script. We’ll set it up to run ‘regular’ scans, once every half an hour.

We need to run our script as root so

 $ sudo su
 $ crontab -e

and then modify the root user crontab file so it looks like this

 0,30 * * * * /home/pi/counter.pl --network home

If you wanted you could also add a second entry to run an ‘enhanced’ scan every so often with ‘–dig’ enabled to attempt to figure out what the mDNS forward address is for each of the discovered hosts. If you haven’t had much experience with crontab before and want to experiment with that, there’s a good introductory how-to on the Raspberry Pi site.

Starting Up

We can also make sure the database is updated every time the Raspberry Pi is rebooted by adding our script to the /etc/rc.local file:

 #!/bin/sh -e
 #
 # rc.local

 # su pi -c '/usr/local/bin/kismet_server -n -c mon1 --daemonize'
 /home/pi/counter.pl --network kaleider &

 exit 0

Building the Giant Display

So far there hasn’t been a lot of hardware hacking. That’s about to change. With our Perl script running every half hour we now have a running count of how many devices there are on the network.

Photo by Luke Arztz

The only soldering needed for the entire project is attaching the driver boards to the back of the giant seven-segment display boards. Fortunately Sparkfun have provided a really in-depth hookup guide to walk you through the process, as well as some excellent advice on how to solder castellated mounting holes if you’re not that familiar with doing so.

Photo by Luke Arztz

Go ahead follow the Sparkfun instructions and attach driver backpacks to the two seven segment displays. The grab your Arduino Uno and wire the two panels up.

Photo by Luke Arztz

While your Arduino is connected to your laptop via USB cable, you can’t power the displays from that, that’s what our 12V power supply is for. So remember to plug that into the barrel connector of the board. At which point you should have both a USB cable (to your laptop) and a power cable coming out of the Arduino.

Photo by Luke Arztz

There’s some excellent example code as part of the Sparkfun hookup guide. So once everything is wired together load it onto the Arduino so we can test out your soldering.

 /*
 Controlling large 7-segment displays
 By: Nathan Seidle
 SparkFun Electronics
 Date: February 25th, 2015
 License: This code is public domain but you buy me a beer if you use this and we meet someday (Beerware license).

 This code demonstrates how to post two numbers to a 2-digit display usings two large digit driver boards.

 Here's how to hook up the Arduino pins to the Large Digit Driver IN

 Arduino pin 6 -> CLK (Green on the 6-pin cable)
 5 -> LAT (Blue)
 7 -> SER on the IN side (Yellow)
 5V -> 5V (Orange)
 Power Arduino with 12V and connect to Vin -> 12V (Red)
 GND -> GND (Black)

There are two connectors on the Large Digit Driver. ‘IN’ is the input side that should be connected to your microcontroller (the Arduino). ‘OUT’ is the output side that should be connected to the ‘IN’ of additional digits. Each display will use about 150mA with all segments and decimal point on.

*/

 //GPIO declarations
 //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
 byte segmentClock = 6;
 byte segmentLatch = 5;
 byte segmentData = 7;

 //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

 void setup()
 {
 Serial.begin(9600);
 Serial.println("Large Digit Driver Example");

 pinMode(segmentClock, OUTPUT);
 pinMode(segmentData, OUTPUT);
 pinMode(segmentLatch, OUTPUT);

 digitalWrite(segmentClock, LOW);
 digitalWrite(segmentData, LOW);
 digitalWrite(segmentLatch, LOW);
 }

 int number = 0;

 void loop()
 {
 showNumber(number); //Test pattern
 number++;
 number %= 100; //Reset x after 99

 Serial.println(number); //For debugging

 delay(500);
 }

 //Takes a number and displays 2 numbers. Displays absolute value (no negatives)
 void showNumber(float value)
 {
 int number = abs(value); //Remove negative signs and any decimals

 //Serial.print("number: ");
 //Serial.println(number);

 for (byte x = 0 ; x < 2 ; x++)
 {
 int remainder = number % 10;

 postNumber(remainder, false);

 number /= 10;
 }

 //Latch the current segment data
 digitalWrite(segmentLatch, LOW);
 digitalWrite(segmentLatch, HIGH); //Register moves storage register on the rising edge of RCK
 }

 //Given a number, or '-', shifts it out to the display
 void postNumber(byte number, boolean decimal)
 {
 // - A
 // / / F/B
 // - G
 // / / E/C
 // -. D/DP

 #define a 1<<0
 #define b 1<<6
 #define c 1<<5
 #define d 1<<4
 #define e 1<<3
 #define f 1<<1
 #define g 1<<2
 #define dp 1<<7

 byte segments;

 switch (number)
 {
 case 1: segments = b | c; break;
 case 2: segments = a | b | d | e | g; break;
 case 3: segments = a | b | c | d | g; break;
 case 4: segments = f | g | b | c; break;
 case 5: segments = a | f | g | c | d; break;
 case 6: segments = a | f | g | e | c | d; break;
 case 7: segments = a | b | c; break;
 case 8: segments = a | b | c | d | e | f | g; break;
 case 9: segments = a | b | c | d | f | g; break;
 case 0: segments = a | b | c | d | e | f; break;
 case ' ': segments = 0; break;
 case 'c': segments = g | e | d; break;
 case '-': segments = g; break;
 }

 if (decimal) segments |= dp;

 //Clock these bits out to the drivers
 for (byte x = 0 ; x < 8 ; x++)
 {
 digitalWrite(segmentClock, LOW);
 digitalWrite(segmentData, segments & 1 << (7 - x));
 digitalWrite(segmentClock, HIGH); //Data transfers to the register on the rising edge of SRCK
 }
 }

Programming the Arduino

Now everything is working stand alone, we can take Nathan’s example code and modify it so that the Arduino accepts a number on the serial port and then displays it using the two linked seven-segment displays. We can then attach the Arduino to our Raspberry Pi, and modify our original scan script to push the current number of devices to the serial port.

You can grab a copy of the modified Arduino sketch from Github. Looking at the code you’ll notice that I’ve stored the last number passed to the code in EEPROM, this means that when we open and close the serial connection from our Raspberry Pi, the display won’t blank out when the Arduino board is reset as the serial connection is opened by our Perl script.

 #include

 byte segmentClock = 6;
 byte segmentLatch = 5;
 byte segmentData = 7;
 String readString;

 int addr = 0;

 void setup() {
 Serial.begin(9600);
 Serial.println("Network Counter");

 pinMode(segmentClock, OUTPUT);
 pinMode(segmentData, OUTPUT);
 pinMode(segmentLatch, OUTPUT);

 digitalWrite(segmentClock, LOW);
 digitalWrite(segmentData, LOW);
 digitalWrite(segmentLatch, LOW);

 int n = EEPROM.read(addr);
 showNumber(n);

 }

 void loop() {
 while (Serial.available()) {
 char c = Serial.read();
 readString += c;
 delay(2);
 }
 if (readString.length() >0) {
 Serial.println(readString);
 int n = readString.toInt();
 showNumber(n);
 EEPROM.update(addr, n);
 readString = "";
 }
 }

 void showNumber(float value) {
 int number = abs(value);

 for (byte x = 0 ; x < 2 ; x++) {
 int remainder = number % 10;
 postNumber(remainder, false);
 number /= 10;
 }

 digitalWrite(segmentLatch, LOW);
 digitalWrite(segmentLatch, HIGH);
 }

 void postNumber(byte number, boolean decimal) {

 #define a 1<<0
 #define b 1<<6
 #define c 1<<5
 #define d 1<<4
 #define e 1<<3
 #define f 1<<1
 #define g 1<<2
 #define dp 1<<7

 byte segments;

 switch (number) {
 case 1: segments = b | c; break;
 case 2: segments = a | b | d | e | g; break;
 case 3: segments = a | b | c | d | g; break;
 case 4: segments = f | g | b | c; break;
 case 5: segments = a | f | g | c | d; break;
 case 6: segments = a | f | g | e | c | d; break;
 case 7: segments = a | b | c; break;
 case 8: segments = a | b | c | d | e | f | g; break;
 case 9: segments = a | b | c | d | f | g; break;
 case 0: segments = a | b | c | d | e | f; break;
 case ' ': segments = 0; break;
 case 'c': segments = g | e | d; break;
 case '-': segments = g; break;
 }

 if (decimal) segments |= dp;



 for (byte x = 0 ; x < 8 ; x++) {

   digitalWrite(segmentClock, LOW);

   digitalWrite(segmentData, segments & 1 << (7 - x));

   digitalWrite(segmentClock, HIGH);

  }

 }

Upload the modified sketch to the board and open up the Arduino Serial Console, make sure the console is set to 9,600 baud and type a number in and send it to the board. If all goes well the number on the giant seven-segment displays should change.

Connecting the Arduino to the Raspberry P

It’s time to put everything together. Unplug the Arduino board’s USB cable from your laptop and plug it into your Raspberry Pi. If all goes well it should show up as a serial device as soon as it’s plugged in,

 $ ls /dev/tty*
 /dev/tty   /dev/tty19 /dev/tty3   /dev/tty40 /dev/tty51 /dev/tty62
 /dev/tty0   /dev/tty2   /dev/tty30 /dev/tty41 /dev/tty52 /dev/tty63
 /dev/tty1   /dev/tty20 /dev/tty31 /dev/tty42 /dev/tty53 /dev/tty7
 /dev/tty10 /dev/tty21 /dev/tty32 /dev/tty43 /dev/tty54 /dev/tty8
 /dev/tty11 /dev/tty22 /dev/tty33 /dev/tty44 /dev/tty55 /dev/tty9
 /dev/tty12 /dev/tty23 /dev/tty34 /dev/tty45 /dev/tty56 /dev/ttyAMA0
 /dev/tty13 /dev/tty24 /dev/tty35 /dev/tty46 /dev/tty57 /dev/ttyS0
 /dev/tty14 /dev/tty25 /dev/tty36 /dev/tty47 /dev/tty58 /dev/ttyUSB0
 /dev/tty15 /dev/tty26 /dev/tty37 /dev/tty48 /dev/tty59 /dev/ttyprintk
 /dev/tty16 /dev/tty27 /dev/tty38 /dev/tty49 /dev/tty6
 /dev/tty17 /dev/tty28 /dev/tty39 /dev/tty5   /dev/tty60
 /dev/tty18 /dev/tty29 /dev/tty4   /dev/tty50 /dev/tty61

you can see here that it’s shown up on my Pi as /dev/ttyUSB0.

Go ahead and install the following package:

 $ sudo apt-get install libdevice-serialport-perl

and then grab the updated Perl script from Github and save it onto your Raspberry Pi, replacing the previous counter.pl script. Change the /dev/ttyUSB0 to reflect your own set up.

 #!/usr/bin/env perl


 #+
 
 # Name:
 #   counter.pl
 
 # Language:
 #   Perl
 
 # Purpose:
 #   Count the number of devices on a network
 
 # Description
 #   Script uses an ARP scan to counter the number of devices on the network 
 #   and saves this information to an SQLite3 database. Database will be created
 #   the first time the script is run.
 
 # External Modules:
 #   Getopt::Long
 #   DBI
 #   DataTime
 #   Device::SerialPort
 
 # Authors:
 #   ANA: Alasdair Allan (Babilim Light Industries)
 
 # History
 #   06-FEB-2017 (ANA):
 #     Checking for duplicate MAC address with multiple IP. Assuming it's a single device.  

 #   04-FEB-2017 (ANA):
 #     Added serial output to Arduino.
 #   03-FEB-2017 (ANA):
 #    Added optional lookup of mDNS forward address of scanned hosts.
 #   02-JAN-2017 (AA):
 #     Original Version
 
 # Copyright:
 #   Copyright (C) 2017. Babilim Light Industries, Ltd. Released under the MIT License.
 
 #-
 
 # L O A D   M O D U L E S -----------------------------------------------
 
 use strict;
 use warnings;
 
 use DBI;
 use Getopt::Long;
 use DateTime;
 use Device::SerialPort;
 
 # O P T I O N S   H A N D L I N G ---------------------------------------
 
 my ( %opt );
 my $status = GetOptions("network=s" => \$opt{"network"},
                        "dig"       => \$opt{"dig"});
 $opt{"network"} = "network" unless defined $opt{"network"};

 # S E R I A L P O R T --------------------------------------------------

 my $port = Device::SerialPort->new("/dev/ttyUSB0");
 $port->baudrate(9600);
 $port->databits(8);
 $port->parity("none");
 $port->stopbits(1);

 # R U N S C A N -------------------------------------------------------

 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";
 }

 my $duplicate = undef;
 if(defined $csv) {
 if( $csv =~ /$mac/ ) {
 $duplicate = 1;
 }
 }

 if ( !$duplicate ) {
 $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++;
 } else {
 print "Ignoring duplicate MAC '$mac'\n";
 }

 }
 }

 my $time = DateTime->now->iso8601;
 print "\nRESULT\n------\n";
 print "time = $time\n";
 print "count = $count\n";
 print "csv = $csv\n";

 # S E R I A L P O R T --------------------------------------------------

 $port->write($count);

 # S A V E S C A N T O D A T A B A S E ----------------------------------

 $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();

 # L A S T O R D E R S -------------------------------------------------

 $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";

 $port->close();
 $dbh->disconnect();
 exit;

 # -----------------------------------------------------------------------

This script looks a lot like our original counter.pl script. However, it uses the serial library to connect to our Arduino and sends the current count of devices to the display. Go ahead and run the script as before.

And we’re done. You now have a configured, working, and (very) visible network counter.

Conclusion

Going Further

There’s plenty of scope to take this project further, both to increase the amount of information that the scanner is logging and also to add another display or two.

For instance, the folks over at ETH Entrepreneur Club in Zürich have set up a Raspberry Pi to capture wireless packets and count them. They then map the traffic, on a scale from 0 to 10, and display it on a rainbow-colored LED bar graph as an easy way to know how heavily loaded the network is right now. So perhaps a giant LED bar graph to display the amount of TCP traffic monitored on the mon1 interface?

Another alternative could be an emergency “stop the network“ button if you see something odd happening? So many possibilities, so little time.