Rogue DHCP Detection Plugin for Nagios on RedHat

I’m pretty sure this is useless on most versions of Linux because the default DHCP plugin that comes with the Nagios Plugins distribution has this functionality and seems to work just fine everywhere except on RedHat-based distros like RHEL, Centos and Fedora Core. On these systems the default plugin does not seem to work and fails to detect any DHCP servers. This plugin is different to the one I gave instructions for before which tests whether a particular DHCP server is answering requests, this plugin finds rogue servers, it will not alert you if any of your actual DHCP servers are down. Hence, you should probably install both. This plugin is not very polished, it is rough and ready but I know it works on RHEL4. If you’re running a different system you may have to do some minor tweaks but this should serve as an excellent starting point none-the-less.

[tags]Nagios, DHCP, RedHat, RHEL, CentOS, Fedora, Linux[/tags]

This plugin uses the RogueDetect Perl module that I mentioned in my adendum yesterday so the first step is to download that and then copy the files DHCPDetect.pm and OUI.pm to some folder on your server. I created a folder called roguedetect in my Nagios plugin folder and stuck them in there.

This plugin uses the same hack that my other DHCP plugin uses to get round the issues RedHat seems to have with running SUID Perl scrips. Hence there are two files needed, the actual perl script that does the work, and a wrapper script that uses sudo to call the main script. Again a line has to be added to /etc/sudoers to allow Nagios run the script as root. I called the main script dhcp_rogue_check.pl and the wrapper dhcp_rogue_check.sh. The code for the wrapper is:

#!/bin/bash

/usr/bin/sudo /usr/lib/nagios/plugins/check_rogue_dhcp.pl $1 $2 $3

The line to add in to /etc/sudoers is:

nagios ALL=NOPASSWD: /usr/lib/nagios/plugins/check_rogue_dhcp.pl *

The code for check_rogue_dhcp.pl is based on dhcpdetector.pl from the RogueDetect distribution. The code uses two threads, the first one starts up a listener, sends a DHCP DISCOVER and then waits for a specified number of seconds before killing the listener. In theory the child process should be able to write to all file handles opened by the parent but on RHEL that does not work so I had to add in a hack to get the results out of the child and into the main thread. The child writes the details of all rogues it finds to a randomly named temporary file in /tmp which the parent then reads before deleting and formulating it’s response to Nagios. The code is show below and it should need no real editing except perhaps for changing the location that it looks for the RogueDetect Perl modules up near the top.

#!/usr/bin/perl

use strict;

#for roguedetect
use lib qw(/usr/lib/nagios/plugins/roguedetect);
use DHCPDetect;
use OUI;

#
# Read args
#
unless($#ARGV >= 1){
print <<endH;
Invalid arguments - at least two arguments must be supplied:
1) The interface to scan from e.g. eth0
2) A list with the MAC addresses of all authorised DHCP servers separated by ::
3) OPTIONAL - the number of seconds to wait for DHCP responses
endH
exit(3);
}

my $interface = $ARGV&#91;0&#93;;
my @approvedservers = split('::', $ARGV&#91;1&#93;);
my $waitFor = 15;
if($ARGV&#91;2&#93;){
$waitFor = $ARGV&#91;2&#93;;
}
#my $interface = 'eth0';
#my @approvedservers = ('00:06:5b:04:eb:6a', '00:0d:66:2f:20:40');
my $debug = 0;
my $tempFile = "/tmp/dhcpdetect.".time();
`touch $tempFile`;

send_log('5',"using temp file $tempFile");

#
# Get the MAC addresses of this host
#
my @iface = split("\n",`ifconfig | grep "HWaddr"`);
my %ifaces = '';
for my $row (@iface) {
$row =~ /(eth\d\.?\d*)\s+Link encap:Ethernet\s+HWaddr\s+(&#91;\d\:\A-Fa-f&#93;+)\s*/;
$ifaces{$1} = $2;
}

#
# Create the DHCPDetect object
#
my $macaddr = $ifaces{$interface};
# Open a new DHCPDetect object..
my $dhcp = new DHCPDetect( macaddr => $macaddr,
interface => $interface,
debug => $debug,
timeout => 2,
approved => \@approvedservers);

#
# Fork the threads
#

defined(my $pid = fork) or die "Cannot fork: $!";
unless ($pid) {
#child (listener)
# Read packets in a loop until killed
while(1) {
# get 1 packet from scanner
$dhcp->scan();
my $server = $dhcp->server_ip;
my $reply = $dhcp->reply;
my $hwa = $dhcp->svr_hwa;
if($dhcp->server_ip eq '') {
send_log('3',"Packet recieved but has no source ip");
}
elsif(approvedserver($dhcp)) {
send_log('3',"Packet from ". $dhcp->svr_hwa ." is identified as from an official DHCP server");
}
else { # all above conditions failed, so its a rogue DHCP Server!
do_alert($hwa, $server, $reply);
}
}
die("Child is done\n");
}

#parent

# Catch signals so we can kill the child first
$SIG{'QUIT'} = \&sighandler;
$SIG{'INT'} = \&sighandler;
$SIG{'HUP'} = \&sighandler;
$SIG{'TERM'} = \&sighandler;

# Fire off a DHCPDISCOVER
sleep(1); # give the listener a chance to warm up
send_log('3',"Sending a DHCPDISCOVER packet..");
$dhcp->bait();

sleep($waitFor);
kill('TERM',$pid);

#
# read the response from the file and formulate the resonse for nagios
#
close(OUTFILE);
open(INFILE, $tempFile) or die "Failed to open the temporary file for reading: $!\n\n";
my @results = <INFILE>;
close(INFILE);
#`rm $tempFile`;

my $numRogues = scalar(@results);
if($numRogues){
print "CRITICAL - $numRogues rogue DHCP servers detected ";
foreach my $rogue (@results){
chomp $rogue;
print ":: $rogue";
}
print "\n";
exit(2);
}else{
print "OK - no rogue DHCP servers detected in $waitFor seconds\n";
exit(0);
}

#
# helper functions
#

# handel signals sent to the child
sub sighandler {
my $sig = shift;
send_log('3',"GOT SIG$sig. Shutting down CHILD ($pid) and self");
kill('TERM', $pid);
die("Deth by signal\n");
} # sighandler

# check if a server is on the white-list
sub approvedserver {

my $self = shift;

my $s_hwa = $self->svr_hwa();
my $ok = 0;

foreach $a (@{$self->approved}) {
if($a eq $s_hwa) {
send_log('5',"DEBUG: $s_hwa is an approved server, ignoring..");
return(1);
}
}

send_log('5',"DEBUG: $s_hwa is not on the approved server list!");
return(0);

}

# debug function
sub send_log {

my $severity = shift @_;
my $message = shift @_;
my $send_msg = '';

# If the severity is less then or = to current debug level
if ($severity <= $debug) {
$send_msg = 1;
}

# If the severity is 0 then we've gotta throw down
if ($severity == '0') {
$send_msg = 1;
}

if (!$message) {
$message = 'ERROR: No Message Recieved, logging failure';
}

# If the above conditions are met and
# the send_msg is set then go ahead and
# log it
if ($send_msg) {
print $message."\n";
}

}

# function to record a rogue server
sub do_alert {
my $hwa = shift;
my $server = shift;
my $reply = shift;

my $timestamp = localtime();

send_log('3',"Detected a rogue DHCP server! HWA: $hwa");

my $body = "Rogue DHCP Server Detected on $interface -- ";
$body .= "HWA: $hwa (" . OUI::oui($hwa). ") - IP: $server -- ";
$body .= "$timestamp";

send_log('5', "Sending output to temp file:\n$body");
open(OUTFILE, ">>$tempFile") or die "failed to open temp file $tempFile for writting\n\n";
print OUTFILE $body;
close(OUTFILE);

return;

} # do_alert

Finally you just need to add a command into your Nagios config so you can use the plugin. I have set up the script and command to take three arguments, the first is the interface to send the DHCP DISCOVER out on, the second is a list of the MAC addresses of all authorized DHCP servers (and any switches that may be forwarding valid DHCP traffic) separated by ::, and the third is the amount of seconds that the listener should listen for responses to the DHCP DISCOVER. The plugin definition I am using is:

define command{
command_name check_rogue_dhcp
command_line /usr/lib/nagios/plugins/check_rogue_dhcp.sh $ARG1$ $ARG2$ $ARG3$
}

Below is an (anonimized) sample of this plugin in use in my DHCP configuration at work:

define service{
host_name some_server
service_description rogue-dhcp-check
check_command check_rogue_dhcp!eth0!00:00:00:00:00:00::11:11:11:11:11:11!15
contact_groups systems_group
use tpl_service_critical
}

That’s it really, as usual this comes with no promises of guarantees. It works for me and I hope it’s of some use to others too!