#!/usr/bin/perl # # ldap_ddns.pl # ### # Synchronise dhcpd.leases with LDAP database (used as DNS backend) to make # Dynamic DNS possible. # Copyright (C) 2008-2009 Etienne Bagnoud # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # ### # Etienne Bagnoud - 2008/05/28 # Addition and correction : 6 - 10 November 2009 # Major rewrite to use DDS, clean up and use strict : march 2010 # use strict; use Config::File qw(read_config_file); use Net::LDAP; use Net::LDAP::Constant qw(LDAP_SUCCESS); use Net::LDAP::Extension::Refresh; use Getopt::Std; use Data::Dumper; use Net::DHCP::ParseLeaseFile; use Date::Parse; use Linux::Inotify2; use File::Copy; use POSIX; use File::Temp qw(tmpnam); use Sys::Syslog; # Needed every where !!!! my $config; my $anonymBind = 0; my $ldapAuth = "simple"; my $machinesEntry = 1; my $startTls = 0; my $noOutput = 0; my $stopCalled = 0; my $inotWatch; openlog('ldap-ddns', 'cons,pid', 'local1'); ### Functions ### # Add entry to LDAP for DNS sub doAddLDAP { my ($ldap, $config, $ip, $hw, $name, $seeAlso, $expire) = @_; return 1 if ! defined $ldap; return 1 if ! defined $config; return 1 if ! defined $ip; return 1 if ! defined $hw; return 1 if ! defined $name; return 1 if ! defined $expire; # Build entry attributes my $dnsEntryAttrs = [ arecord => $ip, objectclass => [ qw(dnsdomain ieee802device dynamicobject) ], $config->{DNS_NAME_ATTR} => $name, macaddress => $hw ]; # Add seeAlso if we have one ! if($seeAlso ne '') { # seeAlso to found machine push(@$dnsEntryAttrs, seealso => $seeAlso); } # Do add ! my $dn = $config->{DNS_NAME_ATTR} . '=' . $name . ',' . $config->{DNS_SUFFIX}; my $addResult = $ldap->add( $dn, attrs => $dnsEntryAttrs ); my $ret = 0; if($addResult->code != LDAP_SUCCESS()) { $ret = 1; } else { # Delete all object with seealso pointing on our actual object my $res = $ldap->search( base => $config->{RDNS_SUFFIX}, scope => 'sub', filter => '(&(objectclass=' . $config->{DNS_OBJECTCLASS} . ')(seealso=' . $dn . '))', attrs => [ 'seealso' ] ); for(my $i=0; $i < $res->count();$i++) { my $entry = $res->entry($i); $ldap->delete($entry->dn()); } print "ADD->REFRESH\n"; ldapddns_AddReverse($ldap, $config, $ip, $dn, $name . '.' . $config->{DNS_DOMAIN_NAME}); ldapddns_Refresh($ldap, $config, $dn, $expire); $ret = 0; } return $ret; } sub ldapddns_AddReverse { my ($ldap, $config, $ip, $dn, $cname) = @_; my($rdn, $rrdn) = _ldapddns_BuildReverseDN($config, $ip); my $doAdd = 0; # Delete reverse entry if there's one my $res = $ldap->search( base => $rdn, scope => 'base', filter => '(objectclass=*)', attrs => [ 'dn', 'cnamerecord' ] ); if($res->count() > 0) { my $entry = $res->entry(0); if($entry->exists('cnamerecord')) { if($entry->get_value('cnamerecord') ne $cname) { print "COHERENCE-DN ERROR\n"; $ldap->delete($entry->dn()); $doAdd=1; } } else { print "COHERENCE-NO CNAME\n"; $ldap->delete($entry->dn()); $doAdd=1; } } else { print "COHERENCE-NO ENTRY $dn - $rdn " . $res->count() . " \n"; $doAdd=1; } if($doAdd == 1) { # Add reverse entry $res = $ldap->add( $rdn, attrs => [ $config->{DNS_NAME_ATTR} => $rrdn, seealso => $dn, cnamerecord => $cname, objectclass => [ $config->{DNS_OBJECTCLASS}, 'dynamicObject'] ] ); } return $doAdd; } sub ldapddns_Refresh { my ($ldap, $config, $dn, $expire) = @_; my $res = $ldap->search( base => $config->{RDNS_SUFFIX}, scope => 'sub', filter => '(&(objectclass=' . $config->{DNS_OBJECTCLASS} . ')(seealso=' . $dn . '))', attrs => [ 'dn' ] ); # Don't mind if we have to much reverse entry, this not our job, we just # refresh ! if($res->count() > 0) { for(my $i = 0; $i < $res->count(); $i++) { my $rdn_entry = $res->entry($i); my $rdn = $rdn_entry->dn(); if($rdn ne '') { print "REFRESH\t=>$rdn ($expire)\n"; $ldap->refresh(entryName => $rdn, requestTtl => $expire); } } } print "REFRESH\t=>$dn ($expire)\n"; $ldap->refresh(entryName => $dn, requestTtl => $expire); } sub ldapddns_Add { my ($ldap, $config, $hw, $ip, $hostname, $expire) = @_; my $doAdd = 1; my $res = $ldap->search( base => $config->{DNS_SUFFIX}, scope => 'sub', filter => '(&(objectclass=' . $config->{DNS_OBJECTCLASS} . ')(macaddress=' . $hw . '))', attrs => [ 'dc', 'arecord', 'macaddress', 'seealso', 'entryttl' ] ); if($res->count() > 1) { print "\tOp-add - more than one !\n"; # We cannot have more than one registered mac, delete all ldapddns_Delete($ldap, $config, $hw, $ip); } elsif($res->count() == 1) { # We might update information if needed my $entry = $res->entry(0); if($entry->get_value('dc') ne "$hostname" || $entry->get_value('arecord') ne "$ip") { print "\tOp-add - different value !\n"; ldapddns_Delete($ldap, $config, $hw, $ip); } else { if($entry->exists('entryttl')) { if(abs(int($entry->get_value('entryttl')) - $expire) > int($config->{DDS_LEASE_EXPIRE_DIFF})) { ldapddns_Refresh($ldap, $config, $entry->dn(), $expire); } $doAdd=0; } else { ldapddns_Delete($ldap, $config, $hw, $ip); } } } if($doAdd == 1) { my $seealso=''; my $res = $ldap->search( base => $config->{MACHINES_SUFFIX}, scope => 'sub', filter => '(&(objectclass=' . $config->{MACHINES_OBJECTCLASS} . ')(macaddress=' . $hw . '))', attrs => [ $config->{MACHINES_NAME_ATTR} ] ); if($res->count() == 1) { my $entry = $res->entry(0); $seealso = $entry->dn(); if($entry->exists($config->{MACHINES_NAME_ATTR})) { $hostname = $entry->get_value($config->{MACHINES_NAME_ATTR}); if($hostname =~ /([0-9a-zA-Z]*)\$$/) { $hostname=$1 } } } print "\tOp-add - add ".$ip."::".$hostname."::".$seealso."\n"; doAddLDAP($ldap, $config, $ip, $hw, $hostname, $seealso, $expire); } } sub ldapddns_Delete { my ($ldap, $config, $hw, $ip) = @_; my $res = $ldap->search( base => $config->{DNS_SUFFIX}, scope => 'sub', filter => '(&(objectclass=' . $config->{DNS_OBJECTCLASS} . ')(macaddress=' . $hw . '))', attrs => [ 'dc', 'arecord', 'macaddress'] ); for(my $i = 0; $i < $res->count(); $i++) { my $entry = $res->entry($i); print "\tOp-delete($i)\t" . $entry->dn() . "\n"; if($entry->exists('arecord')) { my ($dn, $rdn) = _ldapddns_BuildReverseDN($config, $entry->get_value('arecord')); my $rev_dns_res = $ldap->search( base => $dn, scope => 'base', filter => '(objectclass=*)', attrs => [ 'dc' ] ); if($rev_dns_res->count()>0) { print "\tOp-delete - reverse dns $dn, $rdn\n"; $ldap->delete($dn); } } $ldap->delete($res->entry($i)); } } sub ldapddns_CleanupRDNS { my ($ldap, $config) = @_; my $res = $ldap->search( base => $config->{RDNS_SUFFIX}, scope => 'sub', filter => '(&(objectclass=' . $config->{DNS_OBJECTCLASS} . ')(seealso=*))', attrs => [ 'seealso'] ); for(my $i = 0; $i<$res->count(); $i++) { my $entry = $res->entry($i); if($entry->exists('seealso')) { my $seealso = $entry->get_value('seealso'); if($seealso ne '') { my $res2 = $ldap->search( base => $seealso, scope => 'base', filter => '(objectclass=*)', attrs => [ 'dn' ] ); if($res2->code != LDAP_SUCCESS) { $ldap->delete($entry->dn()); } } } } } sub ldapddns_CheckCoherence { my ($ldap, $config) = @_; my $res = $ldap->search( base => $config->{DNS_SUFFIX}, scope => 'sub', filter => '(&(objectclass=' . $config->{DNS_OBJECTCLASS} . ')(entryTtl=*))', attrs => [ 'seelaso', $config->{DNS_NAME_ATTR}, 'arecord', 'entryttl', $config->{DNS_NAME_ATTR} ] ); foreach(my $i=0;$i<$res->count();$i++) { my $entry = $res->entry($i); # No entry without IP if(!$entry->exists('arecord')) { $ldap->delete($entry->dn()); } else { my $ip = $entry->get_value('arecord'); if(ldapddns_AddReverse($ldap, $config, $ip, $entry->dn(), $entry->get_value($config->{DNS_NAME_ATTR}) . '.' . $config->{DNS_DOMAIN_NAME})==1) { print "COHERENCE-REFRESH\n"; ldapddns_Refresh($ldap, $config, $entry->dn(), $entry->get_value('entryttl')); } } } } sub _ldapddns_BuildReverseDN { my ($config, $ip) = @_; my @split_ip = split(/\./, $ip); my $dn = ''; my $rdn = $split_ip[@split_ip-1]; for(my $i=@split_ip;$i>0;$i--) { $dn .= $config->{DNS_NAME_ATTR} . '=' . $split_ip[$i-1] . ','; } return ( $dn . $config->{RDNS_SUFFIX}, $rdn ); } sub ddnsLdap { my $e = shift; ### Initialisation ### # Here we start LDAP ... my $ldap = Net::LDAP->new( $config->{SERVER}, scheme => $config->{SCHEME}, version => $config->{VERSION}, port => $config->{PORT}, onerror => 'warn' ); # TODO # #if($startTls) { # my %tlsOptions; # # push(%tlsOptions, verify => $config->{TLS_VERIFY}) if # defined $config->{TLS_VERIFY}; # push(%tlsOptions, sslversion => $config->{SSL_VERSION}) if # defined $config->{SSL_VERSION}; # push(%tlsOptions, capath => $config->{TLS_CAPATH}) if # defined $config->{TLS_CAPATH} and ! defined $config->{TLS_CAFILE}; # push(%tlsOptions, cafile => $config->{TLS_CAFILE}) if # defined $config->{TLS_CAFILE}; # push(%tlsOptions, clientcert => $config->{TLS_CERT}) if # defined $config->{TLS_CERT}; # push(%tlsOptions, clientkey => $config->{TLS_KEYCERT}) if # defined $config->{TLS_KEYCERT}; # push(%tlsOptions, keydecrypt => sub { $config->{TLS_KEYPASS}; }) if # defined $config->{TLS_KEYPASS}; # push(%tlsOptions, ciphers => $config->{TLS_CIPHERS}) if # defined $config->{TLS_CIPHERS}; # # $ldap->start_tls(%tlsOptions); #} # TODO Support other authentication method than simple if($anonymBind) { $ldap->bind; } else { $ldap->bind( $config->{BIND_DN}, password => $config->{BIND_PW} ); } # Begin by cleaning up the whole tree (if needed) and checking coherence ldapddns_CleanupRDNS($ldap, $config); ldapddns_CheckCoherence($ldap, $config); my $workFile = tmpnam(DIR => '/tmp/'); $workFile = $workFile . '.ldap-ddns'; syslog("LOG_INFO", "Temp filename is $workFile"); copy($config->{LEASE_FILE}, $workFile); ### Read leases ### # Get all leases having hardeware type "ethernet" my $leases = Net::DHCP::ParseLeaseFile->new($workFile); my %machines = (); FETCH_LEASE: while(my $lease = $leases->fetch_lease) { next FETCH_LEASE if ! defined $lease->{"hardware"}; next FETCH_LEASE if ! defined $lease->{"hardware"}{"ethernet"}; my $mac = $lease->{"hardware"}{"ethernet"}; $mac =~ s/://g; $machines{$mac} = $lease; } unlink($workFile); ### Process ldap entries ### # We need during LDAP Loop my $ldapDnsRecord = [ "dn", $config->{DNS_NAME_ATTR}, "macaddress", "arecord", "objectclass", "description" ]; my $searchAttributes = [ "dn", $config->{MACHINES_NAME_ATTR}, "macaddress" ]; my %isMachineAlreadyIn = (); my @summary = (); my $machineCount = -1; foreach my $m (keys %machines) { my $opRes = 0; my $searchForMachineName = 0; my $deleteEntry = 0; my $end = str2time($machines{$m}{'ends'}, 'GMT'); if($machines{$m}{"client-hostname"}) { my $machine = lc($machines{$m}{"client-hostname"}); $machine =~ s/[\.,\|\\\/_\s£\$@#=\?'"\(\)°ç\[\]~^\!<>]*//g; $machines{$m}{'client-hostname'} = $machine; } if($machines{$m}{'binding'} eq 'free' or $machines{$m}{'binding'} eq 'abandonned') { $machines{$m}{'action'} = 'delete'; $machines{$m}{'expire'} = 0; } elsif($machines{$m}{'binding'} eq 'active') { $machines{$m}{'action'} = 'add'; $machines{$m}{'expire'} = $end - time; } elsif($end < time) { $machines{$m}{'action'} = 'delete'; $machines{$m}{'expire'} = 0; } else { $machines{$m}{'action'} = 'skip'; $machines{$m}{'expire'} = 0; } if($machines{$m}{'client-hostname'} eq "" and $machines{$m}{'action'} ne 'delete') { $machines{$m}{'action'} = 'skip'; $machines{$m}{'expire'} = 0; } } while(my ($k, $m) = each(%machines)) { if($m->{'action'} eq 'delete') { ldapddns_Delete($ldap, $config, $m->{'hardware'}{'ethernet'}, $m->{'ip'}); } if($m->{'action'} eq 'add') { ldapddns_Add($ldap, $config, $m->{'hardware'}{'ethernet'}, $m->{'ip'}, $m->{'client-hostname'}, $m->{'expire'}); } } # Yeah ! Everything is fine ! $ldap->unbind; $ldap->disconnect; undef %isMachineAlreadyIn; undef @summary; undef $machineCount; undef %machines; return 0; } sub callStop { syslog("LOG_INFO", "Termination called"); $stopCalled=1; $inotWatch->cancel(); } sub printHelp { print <<"EOF"; Options : -d : Daemonize -c : Specifie configuration file -h : Display help EOF } ### Configuration ### sub main { my $ret = 1; my %options=(); getopts("hdc:", \%options); my $configFilePath = '/etc/ldap-ddns/ldap-ddns.conf'; my $inot = new Linux::Inotify2; if (defined $options{h}) { printHelp; exit 0; } # Configuration file path $configFilePath = $options{c} if defined $options{c}; # Reading configuration and set default $config = read_config_file($configFilePath); $config->{SERVER} = "localhost" if ! defined $config->{SERVER}; $anonymBind = 1 if ! defined $config->{BIND_DN}; $machinesEntry = 0 if ! defined $config->{MACHINES_SUFFIX}; $ldapAuth = $config->{SERVER} if ! defined $config->{AUTH}; $startTls = 1 if defined $config->{STARTTLS} and $config->{STARTTLS} eq 'yes'; $config->{MACHINES_OBJECTCLASS} = "ieee802device" if ! defined $config->{MACHINES_OBJECTCLASS}; $config->{DNS_OBJECTCLASS} = 'dnsdomain' if ! defined $config->{DNS_OBJECTCLASS}; $config->{BIND_PW} = "" if ! defined $config->{BIND_PW}; $config->{MACHINES_NAME_ATTR} = 'cn' if ! defined $config->{MACHINES_NAME_ATTR}; $config->{LEASE_FILE} = '/var/lib/dhcp3/dhcpd.leases' if ! defined $config->{LEASE_FILE}; $noOutput = 1 if defined $config->{NO_OUTPUT} and $config->{NO_OUTPUT} eq 'yes'; if (! defined $config->{DNS_SUFFIX}) { syslog("LOG_ERR", "No DNS suffix set, end."); } else { # Daemonize if(defined($options{d})) { chdir('/'); umask(0); open(STDIN, '/dev/null'); open(STDOUT, '/dev/null'); open(STDERR, '/dev/null'); defined(my $pid = fork); exit if $pid; if(defined $config->{USER}) { my $uid = getpwnam($config->{USER}); syslog("LOG_INFO", 'User id ' . $uid); $> = $uid if(defined $uid); $<=$> } if(defined $config->{GROUP}) { my $gid = getgrnam($config->{GROUP}); syslog("LOG_INFO", 'Group id ' . $gid); $) = $gid if(defined $gid); $(=$); } setsid; } # Run ddnsLdap; my $restartWatch = 0; while(($stopCalled eq 0) and ($restartWatch < 10)) { $inotWatch = $inot->watch($config->{LEASE_FILE}, IN_MODIFY | IN_MOVE | IN_DELETE | IN_DELETE_SELF ); if(!defined($inotWatch)) { syslog("LOG_ERR", 'Could not set watch : ' . $!); $ret = 1; } else { # Install signals past that point $SIG{KILL}=\&callStop; $SIG{TERM}=\&callStop; syslog("LOG_INFO", "Set Watch on " . $config->{LEASE_FILE}); WATCHLOOP: while($stopCalled eq 0) { my @events = $inot->read; if(@events > 0) { ddnsLdap; foreach my $event (@events) { if($event->IN_MOVE || $event->IN_DELETE || $event->IN_DELETE_SELF) { syslog("LOG_WARNING", "Watch file moved or deleted"); $inotWatch->cancel(); last WATCHLOOP; } } $restartWatch=0; } else { if($! ne 0 and $stopCalled eq 0) { syslog("LOG_ERR", "Watch failed : $!"); $inotWatch->cancel(); last WATCHLOOP; } } } } syslog("LOG_INFO", "Restart watch $restartWatch"); $restartWatch++; } } return $ret; } my $ret=main; closelog; exit $ret;