1#!/usr/bin/perl 2# 3# Shows current leases. 4# 5# THIS SCRIPT IS PUBLIC DOMAIN, NO RIGHTS RESERVED! 6# 7# I've removed the email addresses of Christian and vom to avoid 8# putting them on spam lists. If either of you would like to have 9# your email in here please send mail to the DHCP bugs list at ISC. 10# 11# 2008-07-13, Christian Hammers 12# 13# 2009-06-?? - added loading progress counter, pulls hostname, adjusted formatting 14# vom 15# 16# 2013-04-22 - added option to choose lease file, made manufacture information 17# optional, sar 18# 19# 2016-01-19 - updated to better trim the manu string and output the hostnames, sar 20# 21# 2016-01-18 - Mainly cosmetics. Eliminated spurious output in "parsable" mode. 22# Provided for the various conventional lease file locations. (cbp) 23# 24# 2019-06-20 - Updated OUI_URL location. -TS 25 26use strict; 27use warnings; 28use POSIX qw(strftime); 29 30my @LEASES = ('/var/db/dhcpd.leases', '/var/lib/dhcp/dhcpd.leases', '/var/lib/dhcp3/dhcpd.leases'); 31my @all_leases; 32my @leases; 33 34my @OUIS = ('/usr/share/misc/oui.txt', '/usr/local/etc/oui.txt'); 35my $OUI_URL = 'http://standards-oui.ieee.org/oui.txt'; 36my $oui; 37 38my %data; 39 40my $opt_format = 'human'; 41my $opt_keep = 'active'; 42 43our $total_leases = 0; 44 45## Return manufactorer name for specified MAC address (aa:bb:cc:dd:ee:ff). 46sub get_manufactorer_for_mac($) { 47 my $manu = "-NA-"; 48 49 if (defined $oui) { 50 $manu = join('-', ($_[0] =~ /^(..):(..):(..):/)); 51 $manu = `grep -i '$manu' $oui | cut -f3`; 52 $manu =~ s/^\s+|\s+$//g; 53 } 54 55 return $manu; 56} 57 58## Read oui.txt or print warning. 59sub check_oui_file() { 60 61 for my $oui_cand (@OUIS) { 62 if ( -r $oui_cand) { 63 $oui = $oui_cand; 64 last; 65 } 66 } 67 68 if (not defined $oui) { 69 print(STDERR "To get manufacturer names please download $OUI_URL "); 70 print(STDERR "to /usr/local/etc/oui.txt\n"); 71 } 72} 73 74## Read current leases file into array. 75sub read_dhcpd_leases() { 76 77 my $db; 78 for my $db_cand (@LEASES) { 79 if ( -r $db_cand) { 80 $db = $db_cand; 81 last; 82 } 83 } 84 die("Cannot find leases db") unless defined $db; 85 open(F, $db) or die("Cannot open $db: $!"); 86 print("Reading leases from $db\n") if $opt_format eq 'human'; 87 my $content = join('', <F>); 88 close(F); 89 @all_leases = split(/lease/, $content); 90 91 foreach my $lease (@all_leases) { 92 if ($lease =~ /^\s+([\.\d]+)\s+{.*starts \d+ ([\/\d\ \:]+);.*ends \d+ ([\/\d\ \:]+);.*ethernet ([a-f0-9:]+);/s) { 93 ++$total_leases; 94 } 95 } 96} 97 98## Add manufactor name and sort out obsolet assignements. 99sub process_leases() { 100 my $gm_now = strftime("%Y/%m/%d %H:%M:%S", gmtime()); 101 my %tmp_leases; # for sorting and filtering 102 103 my $counter = $opt_format eq 'human' ? 1 : 0; 104 105 # parse entries 106 foreach my $lease (@all_leases) { 107 # skip invalid lines 108 next if not ($lease =~ /^\s+([\.\d]+)\s+{.*starts \d+ ([\/\d\ \:]+);.*ends \d+ ([\/\d\ \:]+);.*ethernet ([a-f0-9:]+);(.*client-hostname \"(\S+)\";)*/s); 109 # skip outdated lines 110 next if ($opt_keep eq 'active' and $3 lt $gm_now); 111 112 if ($counter) { 113 my $percent = (($counter / $total_leases)*100); 114 printf "Processing: %2d%% complete\r", $percent; 115 ++$counter; 116 } 117 118 my $hostname = "-NA-"; 119 if ($6) { 120 $hostname = $6; 121 } 122 123 my $mac = $4; 124 my $date_end = $3; 125 my %entry = ( 126 'ip' => $1, 127 'date_begin' => $2, 128 'date_end' => $date_end, 129 'mac' => $mac, 130 'hostname' => $hostname, 131 'manu' => get_manufactorer_for_mac($mac), 132 ); 133 134 $entry{'date_begin'} =~ s#\/#-#g; # long live ISO 8601 135 $entry{'date_end'} =~ s#\/#-#g; 136 137 if ($opt_keep eq 'all') { 138 push(@leases, \%entry); 139 } elsif (not defined $tmp_leases{$mac} or $tmp_leases{$mac}{'date_end'} gt $date_end) { 140 $tmp_leases{$mac} = \%entry; 141 } 142 } 143 144 # In case we used the hash to filtered 145 if (%tmp_leases) { 146 foreach (sort keys %tmp_leases) { 147 my $h = $tmp_leases{$_}; 148 push(@leases, $h); 149 } 150 } 151 152 # print "\n"; 153 154} 155 156# Output all valid leases. 157sub output_leases() { 158 if ($opt_format eq 'human') { 159 printf "%-19s%-16s%-15s%-20s%-20s\n","MAC","IP","hostname","valid until","manufacturer"; 160 print("===============================================================================================\n"); 161 } 162 foreach (@leases) { 163 if ($opt_format eq 'human') { 164 printf("%-19s%-16s%-14.14s %-20s%-20s\n", 165 $_->{'mac'}, # MAC 166 $_->{'ip'}, # IP address 167 $_->{'hostname'}, # hostname 168 $_->{'date_end'}, # Date 169 $_->{'manu'}); # manufactor name 170 } else { 171 printf("MAC %s IP %s HOSTNAME %s BEGIN %s END %s MANUFACTURER %s\n", 172 $_->{'mac'}, 173 $_->{'ip'}, 174 $_->{'hostname'}, 175 $_->{'date_begin'}, 176 $_->{'date_end'}, 177 $_->{'manu'}); 178 } 179 } 180} 181 182# Commandline Processing. 183sub cli_processing() { 184 while (my $arg = shift(@ARGV)) { 185 if ($arg eq '--help') { 186 print( 187 "Prints active DHCP leases.\n\n". 188 "Usage: $0 [options]\n". 189 " --help shows this help\n". 190 " --parsable machine readable output with full dates\n". 191 " --last prints the last (even if end<now) entry for every MAC\n". 192 " --all prints all entries i.e. more than one per MAC\n". 193 " --lease uses the next argument as the name of the lease file\n". 194 " the default is to try /var/db/dhcpd.leases then\n". 195 " /var/lib/dhcp/dhcpd.leases then\n". 196 " /var/lib/dhcp3/dhcpd.leases\n". 197 "\n"); 198 exit(0); 199 } elsif ($arg eq '--parsable') { 200 $opt_format = 'parsable'; 201 } elsif ($arg eq '--last') { 202 $opt_keep = 'last'; 203 } elsif ($arg eq '--all') { 204 $opt_keep = 'all'; 205 } elsif ($arg eq '--lease') { 206 unshift @LEASES, shift(@ARGV); 207 } else { 208 die("Unknown option $arg"); 209 } 210 } 211} 212 213# 214# main() 215# 216cli_processing(); 217check_oui_file(); 218read_dhcpd_leases(); 219process_leases(); 220output_leases(); 221