I use a development network at my work where we constantly connect new devices up to the network for setup and configuration. The problem I came across was that I don't have direct access to the DHCP server, so it can be time consuming to find the IP address of the new device that I just connected up onto this network. Depending on the piece of equipment, at times, I can connect up a physical display to the specific device in order to view the IP address, other times I can nmap the subnet to locate the device, but all of these options takes time.
*** See it in action *** Watch the Video at the bottom of this page
Most of the DHCP handshake is broadcasted throughout the network, so as long as you have access to the same subnet, you can view the DHCP handshake (most of it) with basic software tools like wireshark, without requiring any other special tools or needing to be "inline" with the device receiving the DHCP address. My device specifically looks at all of the DHCP REQUESTs which is where the client sends out its MAC address along with the IP address and Hostname (along with other information as well, which we ignore) it wants to use, to determine the newest IP address on our network and display it onto an LCD screen.
I wrote a python script that runs on a Raspberry Pi 2 (a little overkill, but it's what I had on-hand at the time), connected a small, low cost Nokia 5110 LCD (maybe $3 on ebay), and now keep it on the same subnet as my development network. The script runs tcpdump with some capture filters, parses out the data from DHCP client REQUEST packets, and prints out the latest IP, Mac address, and Host name onto the LCD screen.
You will not see if a device connects to the network that has a statically assigned IP address, as it only looks at DHCP handhshakes.
Setup Raspbian on your Raspberry Pi 2
I'm running the latest image (as of this writing) of raspbian 2015-11-21-raspbian-jessie.img, you'll need to copy the raspbian image over to your sd card and do the basic setup (expand file system, set local, etc). You can follow my guide Raspberry Pi setup without a Display.
Wire up the Nokia 5110 LCD to your Raspberry Pi
The Nokia 5110 LCD uses the SPI protocol to communicate with the Raspberry Pi. You will need to wire the LCD to the Pi header and install a python library to easily write the information needed to the display. I wrote a guide Nokia 5110 LCD on Raspberry Pi that explains in detail on how this is done.
The script below uses the wiring above. Also, for this project I randomly chose to use GPIO 27 (physical pin #13) on the Raspberry Pi 2 header for the backlight led. I used a 39 ohm resistor in series with the LED backlight as my Nokia LCD already has 300 ohm resistors built-in. I also am using Adafruit's Nokia 5110 LCD library to print out the information onto the screen.
Create the Python Script dhcp-snooper.py
I am open to any improvements or suggestions as I'm sure there are items in the code that can be streamlined a little bit more. I did run into some difficulties in writing this script, I wanted to point out.
- Allowing the LED backlight to blink, turn on/off, while not hanging up the rest of the program
I found some useful information on threading in python with google, unfortunately I lost the original link that helped me out specifically with my script.
- Having subprocess.Popen start tcpdump and hang, waiting for its output
I never would have figured this out without http://stackoverflow.com/questions/8495794/python-popen-stdout-readline-hangs. I managed to c&p enough from the answer to get it to work correctly.
Once you have your pi up and running, ssh into the pi using the terminal or putty.
On the Raspberry Pi, create a script directory, and goto that directory
cd
mkdir scripts
cd scripts
Create the python script
nano dhcp-snooper.py
copy and paste the dhcp-snooper.py python script below. Ctrl+x
and y
to save and exit.
#!/usr/bin/env python
'''
dhcp-snooper.py
Requires tcpdump to be installed
Parses output of TCPDump with capture filters to print DHCP requested IP, client mac address and hostnames of clients on the same network
Algis Salys
'''
import subprocess as sub
import re
import time
import fcntl
import os
import threading
################################################################################################
# for Nokia 5110 LCD
################################################################################################
import Adafruit_Nokia_LCD as LCD
import Adafruit_GPIO.SPI as SPI
import RPi.GPIO as GPIO
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
# Pin for LCD Backlight
GPIO.setmode(GPIO.BCM)
LED = 27 # Backlight LED tied to which GPIO (not physical pin if BCM Mode)
GPIO.setup(LED, GPIO.OUT)
# Raspberry Pi hardware SPI config:
DC = 23
RST = 24
SPI_PORT = 0
SPI_DEVICE = 0
# Hardware SPI usage:
disp = LCD.PCD8544(DC, RST, spi=SPI.SpiDev(SPI_PORT, SPI_DEVICE, max_speed_hz=4000000))
# Initialize library
disp.begin(contrast=55)
# Clear display
disp.clear()
disp.display()
# Create image buffer.
# Make sure to create image with mode '1' for 1-bit color.
image = Image.new('1', (LCD.LCDWIDTH, LCD.LCDHEIGHT))
# Fonts
FONTS_DIR = "/usr/local/share/fonts/"
# Load default font
#font = ImageFont.load_default()
# Load Custom font
font = ImageFont.truetype(FONTS_DIR + 'Minecraftia-Regular.ttf', 8)
# Create drawing object.
draw = ImageDraw.Draw(image)
# Draw a white filled box to clear the image.
draw.rectangle((0,0,LCD.LCDWIDTH,LCD.LCDHEIGHT), outline=255, fill=255)
################################################################################################
CONSOLEPRINT = 0 # Set to 1, for printing information in console, mainly for debugging
LISTSIZE = 3 # how many past ip/mac/and host to save in list, used for console printing, LCD too small and no scroll buttons to show more
rip = ""
cmac = ""
rhost = ""
ip = []
mac = []
host = []
def initscreen(): # Start-up Screen
global draw
global image
global font
draw.rectangle((0,0,LCD.LCDWIDTH,LCD.LCDHEIGHT), outline=255, fill=255)
draw.text((8,0), 'DHCP Snooper', font=font)
draw.line((0,14,83,14), fill=0)
draw.rectangle((0,16,83,18), outline=0, fill=0)
draw.text((7,37), 'algissalys.com', font=font)
disp.image(image)
disp.display()
def waiting(): # Listening... Screen
global draw
global image
global font
BlinkLED(2, .2) # blink duration, blink speed
draw.rectangle((0,0,LCD.LCDWIDTH,LCD.LCDHEIGHT), outline=255, fill=255)
draw.text((12,0), 'Listening...', font=font)
draw.line((0,14,83,14), fill=0)
disp.image(image)
disp.display()
def showlcd(ip,mac,host): # print IP...info onto LCD
global draw
global image
global font
BlinkLED(8,.4)
draw.rectangle((0,0,LCD.LCDWIDTH,LCD.LCDHEIGHT), outline=255, fill=255)
draw.text((0,0), ip, font=font)
draw.text((0,13), mac, font=font)
draw.text((0,25), host, font=font)
draw.text((18,37), time.strftime("%I:%M:%S"), font=font)
disp.image(image)
disp.display()
def BlinkLEDThread(secs, delay):
global LED
abort = threading.Event()
threading.Timer(secs, abort.set).start()
while (not abort.isSet()):
GPIO.output(LED, False)
time.sleep(delay)
GPIO.output(LED, True)
time.sleep(delay)
def BlinkLED(secs, delay):
blink = threading.Event()
threading.Thread(target=BlinkLEDThread, args=(secs, delay)).start()
def OnLEDThread(on, delay):
global LED
abort = threading.Event()
threading.Timer(delay, abort.set).start()
while (not abort.isSet()):
GPIO.output(LED,False)
time.sleep(delay)
GPIO.output(LED,True)
def OnLED(delay):
on = threading.Event()
threading.Thread(target=OnLEDThread, args=(on, delay)).start()
def findWholeWord(w):
return re.compile(r'\b({0})\b'.format(w), flags=re.IGNORECASE).search
def popdhcpinfo(i,m,h):
global ip
global mac
global host
global CONSOLEPRINT
global LISTSIZE
if len(ip) >= LISTSIZE: #if list is equal or larger than "listsize" (0-?), remove last in list
ip = ip[1:]
mac = mac[1:]
host = host[1:]
ip.append(i.rstrip('\r\n'))
mac.append(m.rstrip('\r\n'))
host.append(h.rstrip('\r\n'))
if CONSOLEPRINT: # for printing in console
count = 0
for idx, item in reversed(list(enumerate(ip))): # print newest (reversed) one first
if not count:
count = 1
print " "
print "************************************"
print " Newest Client info "
print "************************************"
print " "
print "IP = %s" % item
print "MAC = %s" % mac[idx]
print "Host = %s" % host[idx]
print " "
print "************************************"
newest = (len(ip)) - 1
showlcd(ip[newest],mac[newest],host[newest])
def nonBlockReadline(output):
fd = output.fileno()
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
try:
return output.readline()
except:
return ''
# Show init screen
OnLED(10)
initscreen()
time.sleep(15)
disp.clear()
disp.display()
# Start TCPDump
data = sub.Popen(('sudo', 'tcpdump', '-l', '-s 0', '-vvv', '-n', '((udp port 67) and (udp[247:4] = 0x63350103))'), stdout=sub.PIPE)
# Show Listening...
waiting()
try:
while True:
line = nonBlockReadline(data.stdout)
data.stdout.flush()
while data != "":
line = nonBlockReadline(data.stdout)
if findWholeWord('requested-ip option 50')(line):
rip = line.split(' ')[-1]
elif findWholeWord('client-id option 61')(line):
cmac = line.split(' ')[-1]
elif findWholeWord('hostname option 12')(line):
rhost = line.split('"')[1]
elif findWholeWord('end option 255')(line):
if not rip:
rip = "Not Available"
if not cmac:
cmac = "Not Available"
if not rhost:
rhost = "Not Available"
popdhcpinfo(rip,cmac,rhost)
rip = ""
cmac = ""
rhost = ""
data.stdout.flush()
finally:
GPIO.cleanup()
Make the script dhcp-snooper.py exectable
chmod +x dhcp-snooper.py
Download some custom fonts, I used Minecraftia-Regular.ttf. You can find in a number of font websites, I found mine at http://www.fonts2u.com/category.html?id=14. The default fonts that are loaded using the Adafruit LCD library are too large to fit all of the information needed on the tiny LCD screen. I opted to put the fonts in the /usr/local/share/fonts/ directory. This is defined by the variable FONTS_DIR in the python script. I downloaded the font locally and scp'd them over to my Pi. You could probably do a wget as well...
/usr/local/share/fonts/Minecraftia-Regular.ttf
Here's a picture of the DHCP-Snooper in action
Run the Python script at boot
Now that we tested the script, we want to have it automatically run when the Raspberry Pi boots up.
There are different ways to have the script start at boot, I chose to just include it in the /etc/rc.local file
sudo nano /etc/rc.local
and add this to the bottom of the file (but before the last line exit 0) of rc.local (change the script name/location if needed). Also confirm rc.local is executable. Note: rc.local runs eveything as root
(sleep 8;python /home/pi/scripts/dhcp-snooper.py)&
Video
Here's a short video of the DHCP-Snooper in action
Conclusion
Anyone can have wireshark or tcpdump running in the background to achieve the same results, but to have a simple, small device, with a blinky LED telling me what the newest IP on the network is, certainly saves me time and aggravation. I plan to possible add a clock to the program, to display current time when there is no activity, as well as a web interface (so other people in my work area can utilize this as well).
So far I have been running this on a semi-busy network for a couple of weeks now, and it seems to be holding up. I plan on doing some more debugging over the next few months to see if I find any issues with certain devices's DHCP handshakes, pi locking up, etc.
Update 20160411
After running this for several weeks 24/7 on a busy network, It has crashed two times on me, The LCD went blank and it was no longer showing any information on the screen. I rebooted the Pi and all was well. I did not have time to troubleshoot what was the cause of the crashes. I still plan on creating a webpage to allow remote viewing of the information, possibly in a few weeks.
References:
http://www.algissalys.com/tech-notes/dhcp-filters-using-tcpdump-to-extract-ip-and-mac-address
http://www.algissalys.com/tech-notes/python-to-extract-dhcp-ip-request-and-client-mac-address
http://stackoverflow.com/questions/8495794/python-popen-stdout-readline-hangs