Fork me on GitHub

ISC dhcpd has the ability to always give a specific MAC address the same IP address “lease” using the fixed-address configuration option. This is configured in Vyatta using the static-mapping configuration statement. Unfortunately, since dhcpd doesn’t store fixed-address leases in the dhcpd.leases file, the Vyatta show dhcp leases command doesn’t show anything about them - which makes it difficult to debug anything dhcp-related if all of the hosts on your network are setup for fixed addresses. I found a mention of this on the vyatta forum, and also an open bug (1990) to fix it, but no proposed resolution. Since I came across this problem and happen to know a bit about ISC dhcpd, I developed both a workaround for users (including a perl script) and a possible solution for the Vyatta developers to implement.

Workaround for Users:

There’s no way to get dhcpd to store fixed-address hosts in the dhcpd.leases file (though it’s been discussed on the dhcpd-users mailing list a few times). There is, however, a way to get dhcpd to log every time it sends an ACK to a client. The following Vyatta configuration commands will get dhcpd to log all transactions to syslog, will have rsyslog put that in /var/log/user/dhcpd. Since this log can fill up very quickly on a busy server, the latter two commands will tell logrotate to rotate the log file when it reaches 3000k in size, and keep 5 copies (feel free to adjust to your needs):

# set service dhcp-server global-parameters "log-facility local2;"
# set system syslog file dhcpd facility local2 level debug
# set system syslog file dhcpd archive files 5
# set system syslog file dhcpd archive size 3000

Once this is done, you can tail -f /var/log/user/dhcpd to watch DHCP discover/request/offer/ack in realtime, or grep through the log file for a specific IP or MAC. If you want an easier method, I’ve written a perl script (latest version will always live in my GitHub misc-scripts repo) to grep through /var/log/user/dhcpd and show the most recent DHCPACK for each IP address, sorted by IP. Here’s the code of the simple script, which is more than half comments. To use it, after performing the above steps, all you need to do is login to your Vyatta box, wget https://raw.github.com/jantman/misc-scripts/master/show_dhcp_fixed_ACKs.pl and then perl show_dhcp_fixed_ACKs.pl.

#!/usr/bin/perl

#
# show_dhcp_fixed_ACKs.pl - script to show the most recent DHCP ACKs per IP address for ISC DHCPd,
#   from a log file. Originally written for Vyatta routers that just show the dynamic leases.
#
# To use this, you need to have dhcpd logging to syslog, and your syslog server putting the log file at
# /var/log/user/dhcpd (or a file path specified by the $logfile variable below.
#
# To accomplish this on Vyatta 6.3, run:
# set service dhcp-server global-parameters "log-facility local2;"
# set system syslog file dhcpd facility local2 level debug
# set system syslog file dhcpd archive files 5
# set system syslog file dhcpd archive size 3000
# commit
#
# Copyright 2011 Jason Antman  All Rights Reserved.
# This script is free for use by anyone anywhere, provided that you comply with the following terms:
# 1) Keep this notice and copyright statement intact.
# 2) Send any substantial changes, improvements or bog fixes back to me at the above address.
# 3) If you include this in a product or redistribute it, you notify me, and include my name in the credits or changelog.
#
# The following URL always points to the newest version of this script. If you obtained it from another source, you should
# check here:
# $HeadURL$
# $LastChangedRevision$
#
# CHANGELOG:
# 2011-12-24 jason@jasonantman.com:
#    initial version of script
#
#

use strict;
use warnings;

my $logfile = "/var/log/user/dhcpd";

my %data = ();

open DF, $logfile or die $!;
while ( my $line =  ) {
    if ( $line !~ m/dhcpd: DHCPACK/) { next;}
    $line =~ m/([A-Za-z]+ [0-9]+ [0-9]{1,2}:[0-9]{2}:[0-9]{2}) [^\/x]+ dhcpd: DHCPACK on (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) to ((?:[0-9a-f]{2}[:-]){5}[0-9a-f]{2}) via (.+)/;
    #print "$1==$2==$3==$4==\n" ;
    $data{"$2"}->{'mac'} = "$3";
    $data{"$2"}->{'date'} = "$1";
    $data{"$2"}->{'if'} = "$4";
    $data{"$2"}->{'ip'} = "$2";    
}

printf("%-18s %-20s %-18s %-10s\n", "IP Address", "Hardware Address", "Date", "Interface");
printf("%-18s %-20s %-18s %-10s\n", "----------", "----------------", "----", "---------");

# begin sort by IP address
my @keys =
  map  substr($_, 4) =>
  sort
  map  pack('C4' =>
    /(\d+)\.(\d+)\.(\d+)\.(\d+)/)
    . $_ => (keys %data);
# end sort by IP address

foreach my $key (@keys) {
    printf("%-18s %-20s %-18s %-10s\n", $data{$key}{'ip'}, $data{$key}{'mac'}, $data{$key}{'date'}, $data{$key}{'if'});
}

A solution for Vyatta:

I suggested this to Vyatta in a reply to bug 1990. Since they already use rsyslog which has very powerful processing capabilities, it would be easy to have rsyslog parse the DHCPACK messages in real time and update some data store (flat files or a simple database) with the information. While how to store this would be up to the Vyatta guys, I have some rsyslog configuration to parse DHCPACK messages and update a MySQL database (with two tables; one for most recent ACK per IP address and one for most recent ACK per MAC address) that might be of some use:

$template DHCPACKonIP, "INSERT INTO dhcplog_ip   
       SET   
       server_ip=inet_aton('%fromhost-ip%'),   
       msg_type='DHCPACK',   
       date='%timereported:::date-mysql%',   
       mac_addr='%msg:R,ERE,2,BLANK:DHCPACK on ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) to (([0-9a-f]{2}:){5}[0-9a-f]{2})( \(([^)]+)\))? via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%',   
       client_ip=inet_aton('%msg:R,ERE,1,BLANK:DHCPACK on ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) to (([0-9a-f]{2}:){5}[0-9a-f]{2})( \(([^)]+)\))? via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%'),   
       gateway='%msg:R,ERE,6,BLANK:DHCPACK on ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) to (([0-9a-f]{2}:){5}[0-9a-f]{2})( \(([^)]+)\))? via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%',   
       fullmsg='%msg%'   
       ON DUPLICATE KEY UPDATE   
       server_ip=inet_aton('%fromhost-ip%'),   
       date='%timereported:::date-mysql%',   
       mac_addr='%msg:R,ERE,2,BLANK:DHCPACK on ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) to (([0-9a-f]{2}:){5}[0-9a-f]{2})( \(([^)]+)\))? via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%',   
       gateway='%msg:R,ERE,6,BLANK:DHCPACK on ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) to (([0-9a-f]{2}:){5}[0-9a-f]{2})( \(([^)]+)\))? via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%',   
       fullmsg='%msg%'  
",SQL

$template DHCPACKonMAC, "INSERT INTO dhcplog_mac   
       SET   
       server_ip=inet_aton('%fromhost-ip%'),   
       msg_type='DHCPACK',   
       date='%timereported:::date-mysql%',   
       mac_addr='%msg:R,ERE,2,BLANK:DHCPACK on ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) to (([0-9a-f]{2}:){5}[0-9a-f]{2})( \(([^)]+)\))? via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%',   
       client_ip=inet_aton('%msg:R,ERE,1,BLANK:DHCPACK on ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) to (([0-9a-f]{2}:){5}[0-9a-f]{2})( \(([^)]+)\))? via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%'),   
       gateway='%msg:R,ERE,6,BLANK:DHCPACK on ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) to (([0-9a-f]{2}:){5}[0-9a-f]{2})( \(([^)]+)\))? via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%',   
       fullmsg='%msg%'   
       ON DUPLICATE KEY UPDATE   
       server_ip=inet_aton('%fromhost-ip%'),   
       date='%timereported:::date-mysql%',   
       client_ip=inet_aton('%msg:R,ERE,1,BLANK:DHCPACK on ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) to (([0-9a-f]{2}:){5}[0-9a-f]{2})( \(([^)]+)\))? via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%'),   
       gateway='%msg:R,ERE,6,BLANK:DHCPACK on ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) to (([0-9a-f]{2}:){5}[0-9a-f]{2})( \(([^)]+)\))? via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%',   
       fullmsg='%msg%'  
",SQL

$template DHCPACKtoIP, "INSERT INTO dhcplog_ip   
       SET   
       server_ip=inet_aton('%fromhost-ip%'),   
       msg_type='DHCPACK',   
       date='%timereported:::date-mysql%',   
       mac_addr='%msg:R,ERE,2,BLANK:DHCPACK to ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) \((([0-9a-f]{2}:){5}[0-9a-f]{2})\) via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%',   
       client_ip=inet_aton('%msg:R,ERE,1,BLANK:DHCPACK to ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) \((([0-9a-f]{2}:){5}[0-9a-f]{2})\) via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%'),   
       gateway='%msg:R,ERE,4,BLANK:DHCPACK to ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) \((([0-9a-f]{2}:){5}[0-9a-f]{2})\) via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%',   
       fullmsg='%msg%'   
       ON DUPLICATE KEY UPDATE   
       server_ip=inet_aton('%fromhost-ip%'),   
       date='%timereported:::date-mysql%',   
       mac_addr='%msg:R,ERE,2,BLANK:DHCPACK to ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) \((([0-9a-f]{2}:){5}[0-9a-f]{2})\) via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%',   
       gateway='%msg:R,ERE,4,BLANK:DHCPACK to ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) \((([0-9a-f]{2}:){5}[0-9a-f]{2})\) via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%',   
       fullmsg='%msg%'  
",SQL

$template DHCPACKtoMAC, "INSERT INTO dhcplog_mac   
       SET   
       server_ip=inet_aton('%fromhost-ip%'),   
       msg_type='DHCPACK',   
       date='%timereported:::date-mysql%',   
       mac_addr='%msg:R,ERE,2,BLANK:DHCPACK to ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) \((([0-9a-f]{2}:){5}[0-9a-f]{2})\) via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%',   
       client_ip=inet_aton('%msg:R,ERE,1,BLANK:DHCPACK to ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) \((([0-9a-f]{2}:){5}[0-9a-f]{2})\) via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%'),   
       gateway='%msg:R,ERE,4,BLANK:DHCPACK to ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) \((([0-9a-f]{2}:){5}[0-9a-f]{2})\) via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%',   
       fullmsg='%msg%'   
       ON DUPLICATE KEY UPDATE   
       server_ip=inet_aton('%fromhost-ip%'),   
       date='%timereported:::date-mysql%',   
       client_ip=inet_aton('%msg:R,ERE,1,BLANK:DHCPACK to ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) \((([0-9a-f]{2}:){5}[0-9a-f]{2})\) via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%'),   
       gateway='%msg:R,ERE,4,BLANK:DHCPACK to ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) \((([0-9a-f]{2}:){5}[0-9a-f]{2})\) via (([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|eth0)--end%',   
       fullmsg='%msg%'  
",SQL

:msg, startswith, " DHCPACK on" :ommysql:hostname,database,dbuser,dbpass;DHCPACKonIP
& :ommysql:hostname,database,dbuser,dbpass;DHCPACKonMAC
& ~ ### DISCARD

if $msg startswith ' DHCPACK to' and ( not ( $msg contains 'no client hardware address' ) )   
then :ommysql:hostname,database,dbuser,dbpass;DHCPACKtoMAC
& :ommysql:hostname,database,dbuser,dbpass;DHCPACKtoIP
& ~ ### DISCARD

Comments

comments powered by Disqus