Configure DHCP to update DNS records: Difference between revisions

From SambaWiki
m (/* Add Apparmor rules)
(/* minor update)
(5 intermediate revisions by the same user not shown)
Line 6: Line 6:


This HowTo is based on a Debian OS install, the paths given may be different if you use another OS.
This HowTo is based on a Debian OS install, the paths given may be different if you use another OS.

The script has now been modified to use <code>samba-tool</code> instead of <code>nsupdate</code>, it also can optionally add the <code>macAddress</code> attribute to a computers AD object, this attribute will contain the computers MAC address.




Line 76: Line 78:
# /usr/local/bin/dhcp-dyndns.sh
# /usr/local/bin/dhcp-dyndns.sh
# This script is for secure DDNS updates on Samba 4
# This script is for secure DDNS updates on Samba,
# it can also add the 'macAddress' to the Computers object.
# Version: 0.8.9
#
# Version: 0.9.0
#
# Copyright (C) Rowland Penny 2020
#
# 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
# (at your option) 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 <http://www.gnu.org/licenses/>.
##########################################################################
# #
# You can optionally add the 'macAddress' to the Computers object. #
# Add 'dhcpduser' to the 'Domain Admins' group if used #
# Change the next line to 'yes' to make this happen #
Add_macAddress='no'
# #
##########################################################################
usage() {
echo "USAGE:"
echo " $(basename $0) add ip-address dhcid|mac-address hostname"
echo " $(basename $0) delete ip-address dhcid|mac-address"
}
_KERBEROS () {
# get current time as a number
test=$(date +%d'-'%m'-'%y' '%H':'%M':'%S)
# Note: there have been problems with this
# check that 'date' returns something like
# Check for valid kerberos ticket
#logger "${test} [dyndns] : Running check for valid kerberos ticket"
klist -c "${KRB5CCNAME}" -s
if [ "$?" != "0" ]; then
logger "${test} [dyndns] : Getting new ticket, old one has expired"
kinit -F -k -t /etc/dhcpduser.keytab "${SETPRINCIPAL}"
if [ "$?" != "0" ]; then
logger "${test} [dyndns] : dhcpd kinit for dynamic DNS failed"
exit 1;
fi
fi
}
rev_zone_info () {
local RevZone="$1"
local IP="$2"
local rzoneip=$(echo "$RevZone" | sed 's/\.in-addr.arpa//')
local rzonenum=$(echo "$rzoneip" | sed 's/\./ /g')
local words=($rzonenum)
local numwords="${#words[@]}"
case "$numwords" in
1) # single ip rev zone '192'
ZoneIP=$(echo "${IP}" | awk -F '.' '{print $1}')
RZIP=$(echo "${rzoneip}" | awk -F '.' '{print $3}')
IP2add=$(echo "${IP}" | awk -F '.' '{print $4"."$3"."$2}')
;;
2) # double ip rev zone '168.192'
ZoneIP=$(echo "${IP}" | awk -F '.' '{print $1"."$2}')
RZIP=$(echo "${rzoneip}" | awk -F '.' '{print $2"."$1}')
IP2add=$(echo "${IP}" | awk -F '.' '{print $4"."$3}')
;;
3) # triple ip rev zone '0.168.192'
ZoneIP=$(echo "${IP}" | awk -F '.' '{print $1"."$2"."$3}')
RZIP=$(echo "${rzoneip}" | awk -F '.' '{print $3"."$2"."$1}')
IP2add=$(echo "${IP}" | awk -F '.' '{print $4}')
;;
*) # should never happen
exit 1
;;
esac
echo "$ZoneIP"
echo "$RZIP"
echo "$IP2add"
}
# Uncomment the next line if using a self compiled Samba and adjust for your PREFIX
#PATH="/usr/local/samba/bin:/usr/local/samba/sbin:$PATH"
BINDIR=$(samba -b | grep 'BINDIR' | grep -v 'SBINDIR' | awk '{print $NF}')
BINDIR=$(samba -b | grep 'BINDIR' | grep -v 'SBINDIR' | awk '{print $NF}')
WBINFO="$BINDIR/wbinfo"
WBINFO="$BINDIR/wbinfo"
# DHCP Server hostname
Server=$(hostname -s)
# DNS domain
# DNS domain
Line 91: Line 178:
exit 1
exit 1
fi
fi
# Samba realm
# Samba 4 realm
REALM=$(echo ${domain^^})
REALM=$(echo ${domain^^})
# Additional nsupdate flags (-g already applied), e.g. "-d" for debug
NSUPDFLAGS="-d"
# krbcc ticket cache
# krbcc ticket cache
Line 103: Line 186:
# Kerberos principal
# Kerberos principal
SETPRINCIPAL="dhcpduser@${REALM}"
SETPRINCIPAL="dhcpduser@${REALM}"
# Kerberos keytab
# Kerberos keytab : /etc/dhcpduser.keytab
# krbcc ticket cache : /tmp/dhcp-dyndns.cc
# /etc/dhcpduser.keytab
TESTUSER="$($WBINFO -u | grep 'dhcpduser')"
# krbcc ticket cache
# /tmp/dhcp-dyndns.cc
TESTUSER="$($WBINFO -u) | grep 'dhcpduser')"
if [ -z "${TESTUSER}" ]; then
if [ -z "${TESTUSER}" ]; then
logger "No AD dhcp user exists, need to create it first.. exiting."
logger "No AD dhcp user exists, need to create it first.. exiting."
logger "you can do this by typing the following commands"
logger "you can do this by typing the following commands"
logger "kinit Administrator@${REALM}"
logger "kinit Administrator@${REALM}"
logger "samba-tool user create dhcpduser --random-password --description=\"Unprivileged user for DNS updates via ISC DHCP server\""
logger "samba-tool user create dhcpduser --random-password --description='Unprivileged user for DNS updates via ISC DHCP server'"
logger "samba-tool user setexpiry dhcpduser --noexpiry"
logger "samba-tool user setexpiry dhcpduser --noexpiry"
logger "samba-tool group addmembers DnsAdmins dhcpduser"
logger "samba-tool group addmembers DnsAdmins dhcpduser"
Line 130: Line 211:
# Variables supplied by dhcpd.conf
# Variables supplied by dhcpd.conf
action=$1
action="$1"
ip=$2
ip="$2"
DHCID=$3
DHCID="$3"
name=${4%%.*}
name="${4%%.*}"
usage()
{
echo "USAGE:"
echo " $(basename $0) add ip-address dhcid|mac-address hostname"
echo " $(basename $0) delete ip-address dhcid|mac-address"
}
_KERBEROS () {
# get current time as a number
test=$(date +%d'-'%m'-'%y' '%H':'%M':'%S)
# Note: there have been problems with this
# check that 'date' returns something like
# 04-09-15 09:38:14
# Check for valid kerberos ticket
#logger "${test} [dyndns] : Running check for valid kerberos ticket"
klist -c /tmp/dhcp-dyndns.cc -s
if [ "$?" != "0" ]; then
logger "${test} [dyndns] : Getting new ticket, old one has expired"
kinit -F -k -t /etc/dhcpduser.keytab -c /tmp/dhcp-dyndns.cc "${SETPRINCIPAL}"
if [ "$?" != "0" ]; then
logger "${test} [dyndns] : dhcpd kinit for dynamic DNS failed"
exit 1;
fi
fi
}
# Exit if no ip address or mac-address
# Exit if no ip address or mac-address
Line 170: Line 223:
# Exit if no computer name supplied, unless the action is 'delete'
# Exit if no computer name supplied, unless the action is 'delete'
if [ "${name}" = "" ]; then
if [ -z "${name}" ]; then
if [ "${action}" = "delete" ]; then
if [ "${action}" = "delete" ]; then
name=$(host -t PTR "${ip}" | awk '{print $NF}' | awk -F '.' '{print $1}')
name=$(host -t PTR "${ip}" | awk '{print $NF}' | awk -F '.' '{print $1}')
Line 179: Line 232:
fi
fi
# exit if name contains a space
# Set PTR address
case ${name} in
ptr=$(echo ${ip} | awk -F '.' '{print $4"."$3"."$2"."$1".in-addr.arpa"}')
*\ * ) logger "Invalid hostname '${name}' ...Exiting"
exit
;;
# * ) : ;;
esac
# exit if $name starts with 'dhcp'
# if you do not want computers without a hostname in AD
# uncomment the following block of code.
<nowiki>if [[ $name == dhcp* ]]; then</nowiki>
logger "not updating DNS record in AD, invalid name"
exit 0
fi
## nsupdate ##
## update ##
case "${action}" in
case "${action}" in
add)
add)
_KERBEROS
_KERBEROS
# does host have an existing 'A' record ?
nsupdate -g ${NSUPDFLAGS} << UPDATE
A_REC=$(host -t A "${name}" | awk '{print $NF}')
server 127.0.0.1
# check for dots
realm ${REALM}
<nowiki>if [[ $A_REC == *.* ]]; then</nowiki>
update delete ${name}.${domain} 3600 A
update add ${name}.${domain} 3600 A ${ip}
samba-tool dns delete ${Server} ${domain} "${name}" A ${ip} -k yes
result1="$?"
send
else
UPDATE
result1=$?
result1=0
fi
samba-tool dns add ${Server} ${domain} "${name}" A ${ip} -k yes
result2="$?"
# get existing reverse zones (if any)
nsupdate -g ${NSUPDFLAGS} << UPDATE
ReverseZones=$(samba-tool dns zonelist ${Server} --reverse | grep 'pszZoneName' | awk '{print $NF}')
server 127.0.0.1
if [ -z "$ReverseZones" ]; then
realm ${REALM}
echo "No reverse zone found, not updating"
update delete ${ptr} 3600 PTR
result3='0'
update add ${ptr} 3600 PTR ${name}.${domain}
result4='0'
send
else
UPDATE
for revzone in $ReverseZones
result2=$?
do
;;
rev_zone_info "$revzone" "${ip}"
delete)
if [[ ${ip} = $ZoneIP* ]] && [ "$ZoneIP" = "$RZIP" ]; then
_KERBEROS
host -t PTR ${ip} > /dev/null 2>&1
if [ "$?" -eq 0 ]; then
samba-tool dns delete ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes
result3="$?"
else
result3='0'
fi
samba-tool dns add ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes
result4="$?"
break
else
continue
fi
done
fi
;;
delete)
_KERBEROS
samba-tool dns delete ${Server} ${domain} "${name}" A ${ip} -k yes
nsupdate -g ${NSUPDFLAGS} << UPDATE
result1="$?"
server 127.0.0.1
# get existing reverse zones (if any)
realm ${REALM}
ReverseZones=$(samba-tool dns zonelist ${Server} --reverse | grep 'pszZoneName' | awk '{print $NF}')
update delete ${name}.${domain} 3600 A
if [ -z "$ReverseZones" ]; then
send
logger "No reverse zone found, not updating"
UPDATE
result2='0'
result1=$?
else
for revzone in $ReverseZones
nsupdate -g ${NSUPDFLAGS} << UPDATE
do
server 127.0.0.1
rev_zone_info "$revzone" "${ip}"
realm ${REALM}
if [[ ${ip} = $ZoneIP* ]] && [ "$ZoneIP" = "$RZIP" ]; then
update delete ${ptr} 3600 PTR
host -t PTR ${ip} > /dev/null 2>&1
send
if [ "$?" -eq 0 ]; then
UPDATE
samba-tool dns delete ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes
result2=$?
result2="$?"
;;
else
*)
result2='0'
echo "Invalid action specified"
fi
exit 103
break
;;
else
continue
fi
done
fi
result3='0'
result4='0'
;;
*)
logger "Invalid action specified"
exit 103
;;
esac
esac
result="${result1}${result2}"
result="${result1}:${result2}:${result3}:${result4}"
if [ "${result}" != "00" ]; then
if [ "${result}" != "0:0:0:0" ]; then
logger "DHCP-DNS Update failed: ${result}"
logger "DHCP-DNS Update failed: ${result}"
exit 1
else
else
logger "DHCP-DNS Update succeeded"
logger "DHCP-DNS Update succeeded"
fi
fi
if [ "$Add_macAddress" != 'no' ]; then
exit ${result}
Computer_Object=$(ldbsearch -k yes -H ldap://"$Server" "(&(objectclass=computer)(objectclass=ieee802Device)(cn=$name))" | grep -v '#' | grep -v 'ref:')
if [ -z "$Computer_Object" ]; then
# Computer object not found with the 'ieee802Device' objectclass, does the computer actually exist, it should.
Computer_Object=$(ldbsearch -k yes -H ldap://"$Server" "(&(objectclass=computer)(cn=$name))" | grep -v '#' | grep -v 'ref:')
if [ -z "$Computer_Object" ]; then
logger "Computer '$name' not found. Exiting."
exit 68
else
DN=$(echo "$Computer_Object" | grep 'dn:')
objldif="$DN
changetype: modify
add: objectclass
objectclass: ieee802Device"
attrldif="$DN
changetype: modify
add: macAddress
macAddress: $DHCID"
# add the ldif
echo "$objldif" | ldbmodify -k yes -H ldap://"$Server"
ret="$?"
if [ "$ret" -ne 0 ]; then
logger "Error modifying Computer objectclass $name in AD."
exit "${ret}"
fi
sleep 2
echo "$attrldif" | ldbmodify -k yes -H ldap://"$Server"
ret="$?"
if [ "$ret" -ne 0 ]; then
logger "Error modifying Computer attribute $name in AD."
exit "${ret}"
fi
unset objldif
unset attrldif
logger "Successfully modified Computer $name in AD"
fi
else
DN=$(echo "$Computer_Object" | grep 'dn:')
attrldif="$DN
changetype: modify
replace: macAddress
macAddress: $DHCID"
echo "$attrldif" | ldbmodify -k yes -H ldap://"$Server"
ret="$?"
if [ "$ret" -ne 0 ]; then
logger "Error modifying Computer attribute $name in AD."
exit "${ret}"
fi
unset attrldif
logger "Successfully modified Computer $name in AD"
fi
fi
exit 0

If you wish to store the computers MAC address in AD, find this line:

Add_macAddress='no'


It is near the top of the script. Change 'no' to 'yes'




Line 247: Line 407:




It has been reported that on raspbian stretch, you have to specify the zone in the reverse update command, if you find that you have problems updating the reverse zone, you can make the following changes to the script above.

Find these lines:
# Set PTR address and its reverse lookup zone
ptr=$(echo ${ip} | awk -F '.' '{print $4"."$3"."$2"."$1".in-addr.arpa"}')

and add this line immediately after them:
rzone=$(echo ${ip} | awk -F '.' '{print $3"."$2"."$1".in-addr.arpa"}')

Now, in the 'add)' section of the '## nsupdate ##' command, change this:

nsupdate -g ${NSUPDFLAGS} << UPDATE
server 127.0.0.1
realm ${REALM}
update delete ${ptr} 3600 PTR
update add ${ptr} 3600 PTR ${name}.${domain}
send
UPDATE
result2=$?
;;

To this:

nsupdate -g ${NSUPDFLAGS} << UPDATE
server 127.0.0.1
realm ${REALM}
zone ${rzone}
update delete ${ptr} 3600 PTR
update add ${ptr} 3600 PTR ${name}.${domain}
send
UPDATE
result2=$?
;;




Line 312: Line 439:
set noname = concat("dhcp-", binary-to-ascii(10, 8, "-", leased-address));
set noname = concat("dhcp-", binary-to-ascii(10, 8, "-", leased-address));
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientDHCID = binary-to-ascii(16, 8, ":", hardware);
set ClientDHCID = concat (
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,1,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,2,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,3,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,4,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,5,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,6,1))),2)
);
set ClientName = pick-first-value(option host-name, config-option-host-name, client-name, noname);
set ClientName = pick-first-value(option host-name, config-option-host-name, client-name, noname);
log(concat("Commit: IP: ", ClientIP, " DHCID: ", ClientDHCID, " Name: ", ClientName));
log(concat("Commit: IP: ", ClientIP, " DHCID: ", ClientDHCID, " Name: ", ClientName));
Line 320: Line 454:
on release {
on release {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientDHCID = binary-to-ascii(16, 8, ":", hardware);
set ClientDHCID = concat (
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,1,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,2,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,3,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,4,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,5,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,6,1))),2)
);
log(concat("Release: IP: ", ClientIP));
log(concat("Release: IP: ", ClientIP));
execute("/usr/local/bin/dhcp-dyndns.sh", "delete", ClientIP, ClientDHCID);
execute("/usr/local/bin/dhcp-dyndns.sh", "delete", ClientIP, ClientDHCID);
Line 335: Line 476:




Start the dhcp server and see what happens, don't forget to stop your windows clients trying to update their own records, as this will fail.
Start the dhcp server and see what happens.


: {{Imbox
| type = important
| text = You must stop your windows clients from trying to update their own records, as this will fail and fill your logs with errors.
}}




Line 350: Line 497:
max-unacked-updates 10;
max-unacked-updates 10;
mclt 3600;
mclt 3600;
split 128;
split 255;
load balance max seconds 3;
load balance max seconds 3;
}
}
Line 423: Line 570:
Once you are sure everything is working as expected, restart both servers to ensure everything is running correctly.
Once you are sure everything is working as expected, restart both servers to ensure everything is running correctly.


The 'split' value '128' on the 'primary', divides responsibility for the clients between the two failover partners.
The 'split' value '255' on the 'primary', makes it responsible for the clients.
If you want the primary to answer all dhcp requests unless it is down (for whatever reason) set the value to '255', use '0' to make the secondary responsible.
With the value set to '255', the primary will answer all dhcp requests unless it is down (for whatever reason), use '0' to make the secondary responsible.


For more information, read the dhcpd.conf manpage <code>man dhcpd.conf</code>.
For more information, read the dhcpd.conf manpage <code>man dhcpd.conf</code>.
Line 440: Line 587:
/etc/dhcpd{,6}.conf r,
/etc/dhcpd{,6}.conf r,
/etc/dhcpd{,6}_ldap.conf r,
/etc/dhcpd{,6}_ldap.conf r,
/etc/dhcp/bin/dhcp-dyndns.sh ix,
/usr/local/bin/dhcp-dyndns.sh ix,
/bin/grep rix,
/bin/grep rix,
/usr/sbin/samba rix,
/usr/sbin/samba rix,

Revision as of 12:25, 21 May 2020

Introduction

This HowTo describes how to configure isc DHCP to update a Samba DC BIND DNS backend. See Setting_up_a_BIND_DNS_Server for how to set up Bind.

It has not been tested with the Samba AD internal DNS server and it probably will not work with the Samba AD internal DNS.

This HowTo is based on a Debian OS install, the paths given may be different if you use another OS.

The script has now been modified to use samba-tool instead of nsupdate, it also can optionally add the macAddress attribute to a computers AD object, this attribute will contain the computers MAC address.


Preconditions

  • The computer has been provisioned as an AD DC and the samba, smbd and winbindd daemons are running.
  • Bind9_dlz is installed and working on the Samba AD DC, tested with various 9.x versions.
  • You have created the reverse zone.
  • You are logged into the DC as 'root'
  • You are doing this on the same DC as Bind9 is installed on


Names and Addresses used in this howto

  • Realm  : SAMDOM.EXAMPLE.COM
  • Subnet  : 192.168.0.0
  • Netmask  : 255.255.255.0
  • Subnet-mask  : 255.255.255.0
  • Broadcast-address  : 192.168.0.255
  • Gateway  : 192.168.0.1
  • Domain-name  : samdom.example.com
  • Domain-name-servers  : 192.168.0.6, 192.168.0.5
  • Netbios-name-servers : 192.168.0.5, 192.168.0.6
  • Ntp-servers  : 192.168.0.5, 192.168.0.6;
  • Pool range  : 192.168.0.50 192.168.0.229


Install isc DHCP

First install the DHCP server

# apt-get install isc-dhcp-server


Create a user to carry out the updates

You need a user that the script will run as, set a random password because you will never logon as the user.

# samba-tool user create dhcpduser --description="Unprivileged user for TSIG-GSSAPI DNS updates via ISC DHCP server" --random-password

Now set the users password to never expire and add the user to the DnsAdmins group.

# samba-tool user setexpiry dhcpduser --noexpiry
# samba-tool group addmembers DnsAdmins dhcpduser

Now export the required keytab.

# samba-tool domain exportkeytab --principal=dhcpduser@SAMDOM.EXAMPLE.COM /etc/dhcpduser.keytab
# chown root:root  /etc/dhcpduser.keytab
# chmod 400  /etc/dhcpduser.keytab


Create the script for the updates

Copy this script to /usr/local/bin/dhcp-dyndns.sh

#!/bin/bash

# /usr/local/bin/dhcp-dyndns.sh

# This script is for secure DDNS updates on Samba,
# it can also add the 'macAddress' to the Computers object.
#
# Version: 0.9.0
#
# Copyright (C) Rowland Penny 2020
#
# 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
# (at your option) 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 <http://www.gnu.org/licenses/>.

##########################################################################
#                                                                        #
#    You can optionally add the 'macAddress' to the Computers object.    #
#    Add 'dhcpduser' to the 'Domain Admins' group if used                #
#    Change the next line to 'yes' to make this happen                   #
Add_macAddress='no'
#                                                                        #
##########################################################################

usage() {
echo "USAGE:"
echo "  $(basename $0) add ip-address dhcid|mac-address hostname"
echo "  $(basename $0) delete ip-address dhcid|mac-address"
}

_KERBEROS () {
# get current time as a number
test=$(date +%d'-'%m'-'%y' '%H':'%M':'%S)
# Note: there have been problems with this
# check that 'date' returns something like

# Check for valid kerberos ticket
#logger "${test} [dyndns] : Running check for valid kerberos ticket"
klist -c "${KRB5CCNAME}" -s
if [ "$?" != "0" ]; then
    logger "${test} [dyndns] : Getting new ticket, old one has expired"
    kinit -F -k -t /etc/dhcpduser.keytab "${SETPRINCIPAL}"
    if [ "$?" != "0" ]; then
        logger "${test} [dyndns] : dhcpd kinit for dynamic DNS failed"
        exit 1;
    fi
fi
}

rev_zone_info () {
    local RevZone="$1"
    local IP="$2"
    local rzoneip=$(echo "$RevZone" | sed 's/\.in-addr.arpa//')
    local rzonenum=$(echo "$rzoneip" | sed 's/\./ /g')
    local words=($rzonenum)
    local numwords="${#words[@]}"
    case "$numwords" in
        1) # single ip rev zone '192'
           ZoneIP=$(echo "${IP}" | awk -F '.' '{print $1}')
           RZIP=$(echo "${rzoneip}" | awk -F '.' '{print $3}')
           IP2add=$(echo "${IP}" | awk -F '.' '{print $4"."$3"."$2}')
           ;;
        2) # double ip rev zone '168.192'
           ZoneIP=$(echo "${IP}" | awk -F '.' '{print $1"."$2}')
           RZIP=$(echo "${rzoneip}" | awk -F '.' '{print $2"."$1}')
           IP2add=$(echo "${IP}" | awk -F '.' '{print $4"."$3}')
           ;;
        3) # triple ip rev zone '0.168.192'
           ZoneIP=$(echo "${IP}" | awk -F '.' '{print $1"."$2"."$3}')
           RZIP=$(echo "${rzoneip}" | awk -F '.' '{print $3"."$2"."$1}')
           IP2add=$(echo "${IP}" | awk -F '.' '{print $4}')
           ;;
        *) # should never happen
           exit 1
           ;;
    esac
    echo "$ZoneIP"
    echo "$RZIP"
    echo "$IP2add"

}

BINDIR=$(samba -b | grep 'BINDIR' | grep -v 'SBINDIR' | awk '{print $NF}')
WBINFO="$BINDIR/wbinfo"

# DHCP Server hostname
Server=$(hostname -s)

# DNS domain
domain=$(hostname -d)
if [ -z ${domain} ]; then
    logger "Cannot obtain domain name, is DNS set up correctly?"
    logger "Cannot continue... Exiting."
    exit 1
fi
# Samba realm
REALM=$(echo ${domain^^})

# krbcc ticket cache
export KRB5CCNAME="/tmp/dhcp-dyndns.cc"

# Kerberos principal
SETPRINCIPAL="dhcpduser@${REALM}"
# Kerberos keytab : /etc/dhcpduser.keytab
# krbcc ticket cache : /tmp/dhcp-dyndns.cc
TESTUSER="$($WBINFO -u | grep 'dhcpduser')"
if [ -z "${TESTUSER}" ]; then
    logger "No AD dhcp user exists, need to create it first.. exiting."
    logger "you can do this by typing the following commands"
    logger "kinit Administrator@${REALM}"
    logger "samba-tool user create dhcpduser --random-password --description='Unprivileged user for DNS updates via ISC DHCP server'"
    logger "samba-tool user setexpiry dhcpduser --noexpiry"
    logger "samba-tool group addmembers DnsAdmins dhcpduser"
    exit 1
fi

# Check for Kerberos keytab
if [ ! -f /etc/dhcpduser.keytab ]; then
    echo "Required keytab /etc/dhcpduser.keytab not found, it needs to be created."
    echo "Use the following commands as root"
    echo "samba-tool domain exportkeytab --principal=${SETPRINCIPAL} /etc/dhcpduser.keytab"
    echo "chown XXXX:XXXX /etc/dhcpduser.keytab"
    echo "Replace 'XXXX:XXXX' with the user & group that dhcpd runs as on your distro"
    echo "chmod 400 /etc/dhcpduser.keytab"
    exit 1
fi

# Variables supplied by dhcpd.conf
action="$1"
ip="$2"
DHCID="$3"
name="${4%%.*}"

# Exit if no ip address or mac-address
if [ -z "${ip}" ] || [ -z "${DHCID}" ]; then
    usage
    exit 1
fi

# Exit if no computer name supplied, unless the action is 'delete'
if [ -z "${name}" ]; then
    if [ "${action}" = "delete" ]; then
        name=$(host -t PTR "${ip}" | awk '{print $NF}' | awk -F '.' '{print $1}')
    else
        usage
        exit 1;
    fi
fi

# exit if name contains a space
case ${name} in
  *\ * ) logger "Invalid hostname '${name}' ...Exiting"
         exit
         ;;
  #  * ) : ;;
esac

# exit if $name starts with 'dhcp'
# if you do not want computers without a hostname in AD
# uncomment the following block of code.
if [[ $name == dhcp* ]]; then
    logger "not updating DNS record in AD, invalid name"
    exit 0
fi

## update ##
case "${action}" in
    add)
        _KERBEROS

        # does host have an existing 'A' record ?
        A_REC=$(host -t A "${name}" | awk '{print $NF}')
        # check for dots
        if [[ $A_REC == *.* ]]; then
            samba-tool dns delete ${Server} ${domain} "${name}" A ${ip} -k yes
            result1="$?"
        else
            result1=0
        fi
        samba-tool dns add ${Server} ${domain} "${name}" A ${ip} -k yes
        result2="$?"

        # get existing reverse zones (if any)
        ReverseZones=$(samba-tool dns zonelist ${Server} --reverse | grep 'pszZoneName' | awk '{print $NF}')
        if [ -z "$ReverseZones" ]; then
            echo "No reverse zone found, not updating"
            result3='0'
            result4='0'
        else
            for revzone in $ReverseZones
            do
              rev_zone_info "$revzone" "${ip}"
              if [[ ${ip} = $ZoneIP* ]] && [ "$ZoneIP" = "$RZIP" ]; then
                  host -t PTR ${ip} > /dev/null 2>&1
                  if [ "$?" -eq 0 ]; then
                      samba-tool dns delete ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes
                      result3="$?"
                  else
                      result3='0'
                  fi
                  samba-tool dns add ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes
                  result4="$?"
                  break
              else
                  continue
              fi
            done
        fi
        ;;
 delete)
        _KERBEROS

        samba-tool dns delete ${Server} ${domain} "${name}" A ${ip} -k yes
        result1="$?"
        # get existing reverse zones (if any)
        ReverseZones=$(samba-tool dns zonelist ${Server} --reverse | grep 'pszZoneName' | awk '{print $NF}')
        if [ -z "$ReverseZones" ]; then
            logger "No reverse zone found, not updating"
            result2='0'
        else
            for revzone in $ReverseZones
            do
              rev_zone_info "$revzone" "${ip}"
              if [[ ${ip} = $ZoneIP* ]] && [ "$ZoneIP" = "$RZIP" ]; then
                  host -t PTR ${ip} > /dev/null 2>&1
                  if [ "$?" -eq 0 ]; then
                      samba-tool dns delete ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes
                      result2="$?"
                  else
                      result2='0'
                  fi
                  break
              else
                  continue
              fi
            done
        fi
        result3='0'
        result4='0'
        ;;
      *)
        logger "Invalid action specified"
        exit 103
        ;;
esac

result="${result1}:${result2}:${result3}:${result4}"

if [ "${result}" != "0:0:0:0" ]; then
    logger "DHCP-DNS Update failed: ${result}"
    exit 1
else
    logger "DHCP-DNS Update succeeded"
fi

if [ "$Add_macAddress" != 'no' ]; then
    Computer_Object=$(ldbsearch -k yes -H ldap://"$Server" "(&(objectclass=computer)(objectclass=ieee802Device)(cn=$name))" | grep -v '#' | grep -v 'ref:')
    if [ -z "$Computer_Object" ]; then
        # Computer object not found with the 'ieee802Device' objectclass, does the computer actually exist, it should.
        Computer_Object=$(ldbsearch -k yes -H ldap://"$Server" "(&(objectclass=computer)(cn=$name))" | grep -v '#' | grep -v 'ref:')
        if [ -z "$Computer_Object" ]; then
            logger "Computer '$name' not found. Exiting."
            exit 68
        else
            DN=$(echo "$Computer_Object" | grep 'dn:')
            objldif="$DN
changetype: modify
add: objectclass
objectclass: ieee802Device"

            attrldif="$DN
changetype: modify
add: macAddress
macAddress: $DHCID"

            # add the ldif
            echo "$objldif" | ldbmodify -k yes -H ldap://"$Server"
            ret="$?"
            if [ "$ret" -ne 0 ]; then
                logger "Error modifying Computer objectclass $name in AD."
                exit "${ret}"
            fi
            sleep 2
            echo "$attrldif" | ldbmodify -k yes -H ldap://"$Server"
            ret="$?"
            if [ "$ret" -ne 0 ]; then
                logger "Error modifying Computer attribute $name in AD."
                exit "${ret}"
            fi
            unset objldif
            unset attrldif
            logger "Successfully modified Computer $name in AD"
        fi
    else
        DN=$(echo "$Computer_Object" | grep 'dn:')
        attrldif="$DN
changetype: modify
replace: macAddress
macAddress: $DHCID"

        echo "$attrldif" | ldbmodify -k yes -H ldap://"$Server"
        ret="$?"
        if [ "$ret" -ne 0 ]; then
            logger "Error modifying Computer attribute $name in AD."
            exit "${ret}"
        fi
        unset attrldif
        logger "Successfully modified Computer $name in AD"
    fi
fi
exit 0

If you wish to store the computers MAC address in AD, find this line:

Add_macAddress='no'

It is near the top of the script. Change 'no' to 'yes'


Set the permissions on the script.

# chmod 755 /usr/local/bin/dhcp-dyndns.sh



Modify the dhcp conf file

First backup the original conf file.

# cp /etc/dhcp/dhcpd.conf /etc/dhcp/dhcpd.conf.orig

Now edit /etc/dhcp/dhcpd.conf and make it look similar to the this.

authoritative;
ddns-update-style none;

subnet 192.168.0.0 netmask 255.255.255.0 {
  option subnet-mask 255.255.255.0;
  option broadcast-address 192.168.0.255;
  option time-offset 0;
  option routers 192.168.0.1;
  option domain-name "samdom.example.com";
  option domain-name-servers 192.168.0.6, 192.168.0.5;
  option netbios-name-servers 192.168.0.5, 192.168.0.6;
  option ntp-servers 192.168.0.5, 192.168.0.6;
  pool {
    max-lease-time 1800; # 30 minutes
    range 192.168.0.50 192.168.0.229;
  }
}

on commit {
set noname = concat("dhcp-", binary-to-ascii(10, 8, "-", leased-address));
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientDHCID = concat (
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,1,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,2,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,3,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,4,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,5,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,6,1))),2)
);
set ClientName = pick-first-value(option host-name, config-option-host-name, client-name, noname);
log(concat("Commit: IP: ", ClientIP, " DHCID: ", ClientDHCID, " Name: ", ClientName));
execute("/usr/local/bin/dhcp-dyndns.sh", "add", ClientIP, ClientDHCID, ClientName);
}

on release {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientDHCID = concat (
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,1,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,2,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,3,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,4,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,5,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,6,1))),2)
);
log(concat("Release: IP: ", ClientIP));
execute("/usr/local/bin/dhcp-dyndns.sh", "delete", ClientIP, ClientDHCID);
}

on expiry {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
# cannot get a ClientMac here, apparently this only works when actually receiving a packet
log(concat("Expired: IP: ", ClientIP));
# cannot get a ClientName here, for some reason that always fails
execute("/usr/local/bin/dhcp-dyndns.sh", "delete", ClientIP, "", "0");
}


Start the dhcp server and see what happens.



Add failover

Add the following to the /etc/dhcp/dhcpd.conf file on the primary:

failover peer "dhcp-failover" {
  primary;
  address dc1.samdom.example.com;
  peer address dc2.samdom.example.com;
  max-response-delay 60;
  max-unacked-updates 10;
  mclt 3600;
  split 255;
  load balance max seconds 3;
}

..and secondary:

failover peer "dhcp-failover" {
  secondary;
  address dc2.samdom.example.com;
  peer address dc1.samdom.example.com;
  max-response-delay 60;
  max-unacked-updates 10;
  load balance max seconds 3;
}


Add references for the subnet/pool which will do failover.

subnet 192.168.0.0 netmask 255.255.255.0 {
  option subnet-mask 255.255.255.0;
  option broadcast-address 192.168.0.255;
  option time-offset 0;
  option routers 192.168.0.1;
  option domain-name "samdom.example.com";
  option domain-name-servers 192.168.0.5, 192.168.0.6;
  option ntp-servers 192.168.0.5, 192.168.0.6;
  pool {
    failover peer "dhcp-failover";
    max-lease-time 1800; # 30 minutes
    range 192.168.0.50 192.168.0.229;
  }
}

Configure OMAPI and define a secret key.

Generate a random OMAPI key on the primary, using the dnssec-keygen utility distributed with BIND.

dnssec‐keygen ‐a HMAC‐MD5 ‐b 512 ‐n USER DHCP_OMAPI

Now extract the actual key:

cat Kdhcp_omapi.+*.private |grep ^Key|cut -d ' ' -f2-

Add the following to dhcpd.conf on both primary and secondary.

omapi-port 7911;
omapi-key omapi_key;

key omapi_key {
     algorithm hmac-md5;
     secret "PUT_YOUR_KEY_HERE";
}

Replace PUT_YOUR_KEY_HERE with the key you extracted from the private key created by the dnssec command

Restart both servers to apply the configuration changes.

You should find lines similar to these, in the system logs on both machines:

Feb 28 17:34:39 dc1 dhcpd: failover peer dhcp-failover: peer moves from recover-done to normal
Feb 28 17:34:39 dc1 dhcpd: failover peer dhcp-failover: Both servers normal

If OMAPI is working properly you can test failover by stopping the primary server.

If you are using a firewall, you will need to open TCP ports 647 and 7911

Once you are sure everything is working as expected, restart both servers to ensure everything is running correctly.

The 'split' value '255' on the 'primary', makes it responsible for the clients. With the value set to '255', the primary will answer all dhcp requests unless it is down (for whatever reason), use '0' to make the secondary responsible.

For more information, read the dhcpd.conf manpage man dhcpd.conf.



Apparmor

To get DHCP updates working with Apparmor, you need to alter /etc/apparmor.d/usr.sbin.dhcpd to match this.

/etc/dhcp/ r,
/etc/dhcp/** r,
/etc/dhcpd{,6}.conf r,
/etc/dhcpd{,6}_ldap.conf r,
/usr/local/bin/dhcp-dyndns.sh ix,
/bin/grep rix,
/usr/sbin/samba rix,
/usr/bin/gawk rix,
/bin/hostname rix,
/usr/bin/wbinfo rix,
/usr/bin/heimtools rix,
/usr/bin/logger rix,
/usr/bin/kinit.heimdal rix,
/bin/date rix,
/dev/tty wr,
/dev/urandom w,
/proc/** r,
/usr/bin/kinit w,
/run/samba/winbindd/pipe wr,


The first 4 lines are the default, you will need to add everything else. With these settings the dhcp-server will start and work.

The above settings have been tested on Ubuntu 18.04 and were supplied by Stefan Kania.


Any questions or problems, ask on the Samba mailing list.