Configure DHCP to update DNS records: Difference between revisions

From SambaWiki
m (*/ script bug fixes)
(dhcp-dyndns.sh: Read output of dns query directly into array to avoid issues with globs)
 
(12 intermediate revisions by 4 users not shown)
Line 1: Line 1:
= Introduction =
= 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.
This HowTo describes how to configure isc DHCP to update Samba dns records in AD.


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


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.
Line 14: Line 14:


* The computer has been provisioned as an AD DC and the samba, smbd and winbindd daemons are running.
* 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 any required reverse zones.
* You have created any required reverse zones.
Line 21: Line 19:
* You are logged into the DC as 'root'
* You are logged into the DC as 'root'


* If using Bind9, Bind9_dlz must be installed and working on the Samba AD DC that you are doing this on. Tested with various 9.x versions.
* You are doing this on the same DC as Bind9 is installed on




Line 46: Line 44:
# apt-get install isc-dhcp-server
# apt-get install isc-dhcp-server


Or on FreeBSD:

# pkg install isc-dhcp44-server


= Create a user to carry out the updates =
= Create a user to carry out the updates =
Line 59: Line 58:
# samba-tool group addmembers DnsAdmins dhcpduser
# samba-tool group addmembers DnsAdmins dhcpduser


Now export the required keytab.
Now export the required keytab. On FreeBSD change <code>/etc/dhcpduser.keytab</code> to <code>/usr/local/etc/dhcpduser.keytab</code>


# samba-tool domain exportkeytab --principal=dhcpduser@SAMDOM.EXAMPLE.COM /etc/dhcpduser.keytab
# samba-tool domain exportkeytab --principal=dhcpduser@SAMDOM.EXAMPLE.COM /etc/dhcpduser.keytab
Line 66: Line 65:
{{Imbox
{{Imbox
| type = note
| type = note
| text = In the <code>chown</code> command above <code>root:root</code> is used, you need to check what user & group DHCP runs as on your distro and if different, change <code>root:root</code> to the correct user & group.
| text = In the <code>chown</code> command above <code>root:root</code> is used, you need to check what user & group DHCP runs as on your distro and if different, change <code>root:root</code> to the correct user & group. On FreeBSD this is <code>dhcpd:dhcpd</code>.
}}
}}


Line 75: Line 74:


#!/bin/bash
#!/bin/bash
# On FreeBSD change the above line to #!/usr/local/bin/bash
#
# /usr/local/bin/dhcp-dyndns.sh
# /usr/local/bin/dhcp-dyndns.sh
#
# This script is for secure DDNS updates on Samba,
# This script is for secure DDNS updates on Samba,
# it can also add the 'macAddress' to the Computers object.
# it can also add the 'macAddress' to the Computers object.
#
#
# Version: 0.9.2
# Version: 0.9.6
#
#
# Copyright (C) Rowland Penny 2020-2021
# Copyright (C) Rowland Penny 2020-2022
#
#
# This program is free software; you can redistribute it and/or modify
# This program is free software; you can redistribute it and/or modify
Line 97: Line 97:
# You should have received a copy of the GNU General Public License
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# You may need to ensure that you have a useful path
# If you have 'path' problems, Uncomment the next line and adjust for
# your setup e.g. self-compiled Samba
#export PATH=/usr/local/samba/bin:/usr/local/samba/sbin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin
##########################################################################
##########################################################################
Line 107: Line 112:
##########################################################################
##########################################################################
# On FreeBSD change this to /usr/local/etc/dhcpduser.keytab
usage() {
keytab=/etc/dhcpduser.keytab
echo "USAGE:"
echo " $(basename $0) add ip-address dhcid|mac-address hostname"
usage()
echo " $(basename $0) delete ip-address dhcid|mac-address"
{
cat <<-EOF
USAGE:
$(basename "$0") add ip-address dhcid|mac-address hostname
$(basename "$0") delete ip-address dhcid|mac-address
EOF
}
}
_KERBEROS () {
_KERBEROS()
{
# get current time as a number
# get current time as a number
test=$(date +%d'-'%m'-'%y' '%H':'%M':'%S)
test=$(date +%d'-'%m'-'%y' '%H':'%M':'%S)
# Note: there have been problems with this
# Note: there have been problems with this
# check that 'date' returns something like
# check that 'date' returns something like
# Check for valid kerberos ticket
# Check for valid kerberos ticket
#logger "${test} [dyndns] : Running check for valid kerberos ticket"
#logger "${test} [dyndns] : Running check for valid kerberos ticket"
klist -c "${KRB5CCNAME}" -s
klist -c "${KRB5CCNAME}" -s
ret="$?"
if [ "$?" != "0" ]; then
if [ $ret -ne 0 ]
logger "${test} [dyndns] : Getting new ticket, old one has expired"
then
kinit -F -k -t /etc/dhcpduser.keytab "${SETPRINCIPAL}"
logger "${test} [dyndns] : Getting new ticket, old one has expired"
if [ "$?" != "0" ]; then
# On FreeBSD change the -F to --no-forwardable
logger "${test} [dyndns] : dhcpd kinit for dynamic DNS failed"
kinit -F -k -t $keytab "${SETPRINCIPAL}"
exit 1
ret="$?"
fi
if [ $ret -ne 0 ]
fi
then
logger "${test} [dyndns] : dhcpd kinit for dynamic DNS failed"
exit 1
fi
fi
}
}
rev_zone_info () {
rev_zone_info()
{
local RevZone="$1"
local IP="$2"
local RevZone="$1"
local rzoneip
local IP="$2"
local rzoneip
rzoneip=$(echo "$RevZone" | sed 's/\.in-addr.arpa//')
rzoneip="${RevZone%.in-addr.arpa}"
local rzonenum
local rzonenum
rzonenum=$(echo "$rzoneip" | tr '.' '\n')
rzonenum=$(echo "$rzoneip" | tr '.' '\n')
declare -a words
declare -a words
for n in $rzonenum
for n in $rzonenum
do
do
words+=("$n")
words+=("$n")
done
done
local numwords="${#words[@]}"
local numwords="${#words[@]}"
unset ZoneIP
unset ZoneIP
unset RZIP
unset RZIP
unset IP2add
unset IP2add
case "$numwords" in
case "$numwords" in
1)
1) # single ip rev zone '192'
# single ip rev zone '192'
ZoneIP=$(echo "${IP}" | awk -F '.' '{print $1}')
ZoneIP=$(echo "${IP}" | awk -F '.' '{print $1}')
RZIP="${rzoneip}"
RZIP="${rzoneip}"
IP2add=$(echo "${IP}" | awk -F '.' '{print $4"."$3"."$2}')
IP2add=$(echo "${IP}" | awk -F '.' '{print $4"."$3"."$2}')
;;
;;
2) # double ip rev zone '168.192'
2)
ZoneIP=$(echo "${IP}" | awk -F '.' '{print $1"."$2}')
# double ip rev zone '168.192'
RZIP=$(echo "${rzoneip}" | awk -F '.' '{print $2"."$1}')
IP2add=$(echo "${IP}" | awk -F '.' '{print $4"."$3}')
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}')
3)
RZIP=$(echo "${rzoneip}" | awk -F '.' '{print $3"."$2"."$1}')
# triple ip rev zone '0.168.192'
IP2add=$(echo "${IP}" | awk -F '.' '{print $4}')
ZoneIP=$(echo "${IP}" | awk -F '.' '{print $1"."$2"."$3}')
;;
RZIP=$(echo "${rzoneip}" | awk -F '.' '{print $3"."$2"."$1}')
*) # should never happen
IP2add=$(echo "${IP}" | awk -F '.' '{print $4}')
exit 1
;;
;;
*)
esac
# should never happen
exit 1
;;
esac
}
}
BINDIR=$(samba -b | grep 'BINDIR' | grep -v 'SBINDIR' | awk '{print $NF}')
BINDIR=$(samba -b | grep 'BINDIR' | grep -v 'SBINDIR' | awk '{print $NF}')
<nowiki>[[ -z $BINDIR ]] && printf "Cannot find the 'samba' binary, is it installed ?\\nOr is your path set correctly ?\\n"</nowiki>
WBINFO="$BINDIR/wbinfo"
WBINFO="$BINDIR/wbinfo"
SAMBATOOL=$(command -v samba-tool)
<nowiki>[[ -z $SAMBATOOL ]] && printf "Cannot find the 'samba-tool' binary, is it installed ?\\nOr is your path set correctly ?\\n"</nowiki>
MINVER=$($SAMBATOOL -V | grep -o '[0-9]*' | tr '\n' ' ' | awk '{print $2}')
if [ "$MINVER" -gt '14' ]
then
KTYPE="--use-kerberos=required"
else
KTYPE="-k yes"
fi
# DHCP Server hostname
# DHCP Server hostname
Line 180: Line 214:
# DNS domain
# DNS domain
domain=$(hostname -d)
domain=$(hostname -d)
if [ -z ${domain} ]; then
if [ -z "${domain}" ]
then
logger "Cannot obtain domain name, is DNS set up correctly?"
logger "Cannot obtain domain name, is DNS set up correctly?"
logger "Cannot continue... Exiting."
logger "Cannot continue... Exiting."
exit 1
exit 1
fi
fi
# Samba realm
# Samba realm
REALM=$(echo ${domain^^})
REALM="${domain^^}"
# krbcc ticket cache
# krbcc ticket cache
Line 193: Line 229:
# Kerberos principal
# Kerberos principal
SETPRINCIPAL="dhcpduser@${REALM}"
SETPRINCIPAL="dhcpduser@${REALM}"
# Kerberos keytab : /etc/dhcpduser.keytab
# Kerberos keytab as above
# krbcc ticket cache : /tmp/dhcp-dyndns.cc
# krbcc ticket cache : /tmp/dhcp-dyndns.cc
TESTUSER="$($WBINFO -u | grep 'dhcpduser')"
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 setexpiry dhcpduser --noexpiry"
logger "$SAMBATOOL user create dhcpduser --random-password --description='Unprivileged user for DNS updates via ISC DHCP server'"
logger "samba-tool group addmembers DnsAdmins dhcpduser"
logger "$SAMBATOOL user setexpiry dhcpduser --noexpiry"
logger "$SAMBATOOL group addmembers DnsAdmins dhcpduser"
exit 1
exit 1
fi
fi
# Check for Kerberos keytab
# Check for Kerberos keytab
if [ ! -f /etc/dhcpduser.keytab ]; then
if [ ! -f "$keytab" ]
then
logger "Required keytab /etc/dhcpduser.keytab not found, it needs to be created."
logger "Required keytab $keytab not found, it needs to be created."
logger "Use the following commands as root"
logger "Use the following commands as root"
logger "samba-tool domain exportkeytab --principal=${SETPRINCIPAL} /etc/dhcpduser.keytab"
logger "$SAMBATOOL domain exportkeytab --principal=${SETPRINCIPAL} $keytab"
logger "chown XXXX:XXXX /etc/dhcpduser.keytab"
logger "Replace 'XXXX:XXXX' with the user & group that dhcpd runs as on your distro"
logger "chown XXXX:XXXX $keytab"
logger "Replace 'XXXX:XXXX' with the user & group that dhcpd runs as on your distro"
logger "chmod 400 /etc/dhcpduser.keytab"
logger "chmod 400 $keytab"
exit 1
exit 1
fi
fi
Line 223: Line 261:
name="${4%%.*}"
name="${4%%.*}"
# Exit if no ip address or mac-address
# Exit if no ip address
if [ -z "${ip}" ]; then
if [ -z "${ip}" ]
then
usage
usage
exit 1
exit 1
fi
fi
# Exit if no computer name supplied, unless the action is 'delete'
# Exit if no computer name supplied, unless the action is 'delete'
if [ -z "${name}" ]; then
if [ -z "${name}" ]
then
if [ "${action}" = "delete" ]; then
if [ "${action}" = "delete" ]
name=$(host -t PTR "${ip}" | awk '{print $NF}' | awk -F '.' '{print $1}')
then
else
name=$(host -t PTR "${ip}" | awk '{print $NF}' | awk -F '.' '{print $1}')
usage
else
exit 1
usage
fi
exit 1
fi
fi
fi
# exit if name contains a space
# exit if name contains a space
case ${name} in
case ${name} in
*\ * )
*\ * ) logger "Invalid hostname '${name}' ...Exiting"
logger "Invalid hostname '${name}' ...Exiting"
exit
exit
;;
;;
esac
esac
# exit if $name starts with 'dhcp'
# if you want computers with a hostname that starts with 'dhcp' in AD
# comment the following block of code.
# if you do not want computers without a hostname in AD
<nowiki>if [[ $name == dhcp* ]]</nowiki>
# uncomment the following block of code.
then
#<nowiki>if [[ $name == dhcp* ]]; then</nowiki>
# logger "not updating DNS record in AD, invalid name"
logger "not updating DNS record in AD, invalid name"
# exit 0
exit 0
#fi
fi
## update ##
## update ##
case "${action}" in
case "${action}" in
add)
add)
_KERBEROS
_KERBEROS
count=0
count=0
# does host have an existing 'A' record ?
# does host have an existing 'A' record ?
A_REC=$(samba-tool dns query ${Server} ${domain} ${name} A -k yes 2>/dev/null | grep 'A:' | awk '{print $2}')
mapfile -t A_REC < <($SAMBATOOL dns query "${Server}" "${domain}" "${name}" A "$KTYPE" 2>/dev/null | grep 'A:' | awk '{print $2}')
if [ "${#A_REC[@]}" -eq 0 ]
<nowiki>if [[ -z $A_REC ]]; then</nowiki>
then
# no A record to delete
# no A record to delete
result1=0
result1=0
samba-tool dns add ${Server} ${domain} "${name}" A ${ip} -k yes
$SAMBATOOL dns add "${Server}" "${domain}" "${name}" A "${ip}" "$KTYPE"
result2="$?"
result2="$?"
elif [ "$A_REC" = "${ip}" ]; then
elif [ "${#A_REC[@]}" -gt 1 ]
# Correct A record exists, do nothing
then
logger "Correct 'A' record exists, not updating."
for i in "${A_REC[@]}"
result1=0
do
result2=0
$SAMBATOOL dns delete "${Server}" "${domain}" "${name}" A "${i}" "$KTYPE"
count=$((count+1))
done
elif [ "$A_REC" != "${ip}" ]; then
# all A records deleted
# Wrong A record exists
result1=0
logger "'A' record changed, updating record."
samba-tool dns delete ${Server} ${domain} "${name}" A ${A_REC} -k yes
$SAMBATOOL dns add "${Server}" "${domain}" "${name}" A "${ip}" "$KTYPE"
result1="$?"
result2="$?"
elif [ "${#A_REC[@]}" -eq 1 ]
samba-tool dns add ${Server} ${domain} "${name}" A ${ip} -k yes
then
result2="$?"
# turn array into a variable
fi
VAR_A_REC="${A_REC[*]}"
if [ "$VAR_A_REC" = "${ip}" ]
then
# Correct A record exists, do nothing
logger "Correct 'A' record exists, not updating."
result1=0
result2=0
count=$((count+1))
elif [ "$VAR_A_REC" != "${ip}" ]
then
# Wrong A record exists
logger "'A' record changed, updating record."
$SAMBATOOL dns delete "${Server}" "${domain}" "${name}" A "${VAR_A_REC}" "$KTYPE"
result1="$?"
$SAMBATOOL dns add "${Server}" "${domain}" "${name}" A "${ip}" "$KTYPE"
result2="$?"
fi
fi
# get existing reverse zones (if any)
# get existing reverse zones (if any)
ReverseZones=$(samba-tool dns zonelist ${Server} -k yes --reverse | grep 'pszZoneName' | awk '{print $NF}')
ReverseZones=$($SAMBATOOL dns zonelist "${Server}" "$KTYPE" --reverse | grep 'pszZoneName' | awk '{print $NF}')
if [ -z "$ReverseZones" ]; then
if [ -z "$ReverseZones" ]; then
logger "No reverse zone found, not updating"
logger "No reverse zone found, not updating"
result3='0'
result3='0'
result4='0'
result4='0'
count=$((count+1))
count=$((count+1))
else
else
for revzone in $ReverseZones
for revzone in $ReverseZones
do
do
rev_zone_info "$revzone" "${ip}"
rev_zone_info "$revzone" "${ip}"
if [[ ${ip} = $ZoneIP* ]] && [ "$ZoneIP" = "$RZIP" ]; then
if [[ ${ip} = $ZoneIP* ]] && [ "$ZoneIP" = "$RZIP" ]
then
# does host have an existing 'PTR' record ?
# does host have an existing 'PTR' record ?
PTR_REC=$(samba-tool dns query ${Server} ${revzone} ${IP2add} PTR -k yes 2>/dev/null | grep 'PTR:' | awk '{print $2}' | awk -F '.' '{print $1}')
PTR_REC=$($SAMBATOOL dns query "${Server}" "${revzone}" "${IP2add}" PTR "$KTYPE" 2>/dev/null | grep 'PTR:' | awk '{print $2}' | awk -F '.' '{print $1}')
<nowiki>if [[ -z $PTR_REC ]]; then</nowiki>
<nowiki>if [[ -z $PTR_REC ]]</nowiki>
# no PTR record to delete
then
result3=0
# no PTR record to delete
samba-tool dns add ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes
result3=0
result4="$?"
$SAMBATOOL dns add "${Server}" "${revzone}" "${IP2add}" PTR "${name}"."${domain}" "$KTYPE"
break
result4="$?"
elif [ "$PTR_REC" = "${name}" ]; then
break
# Correct PTR record exists, do nothing
elif [ "$PTR_REC" = "${name}" ]
logger "Correct 'PTR' record exists, not updating."
then
result3=0
# Correct PTR record exists, do nothing
result4=0
logger "Correct 'PTR' record exists, not updating."
count=$((count+1))
result3=0
break
result4=0
elif [ "$PTR_REC" != "${name}" ]; then
count=$((count+1))
# Wrong PTR record exists
break
# points to wrong host
elif [ "$PTR_REC" != "${name}" ]
logger "'PTR' record changed, updating record."
then
samba-tool dns delete ${Server} ${revzone} ${IP2add} PTR "${PTR_REC}".${domain} -k yes
# Wrong PTR record exists
result3="$?"
# points to wrong host
samba-tool dns add ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes
logger "'PTR' record changed, updating record."
result4="$?"
$SAMBATOOL dns delete "${Server}" "${revzone}" "${IP2add}" PTR "${PTR_REC}"."${domain}" "$KTYPE"
break
result3="$?"
fi
$SAMBATOOL dns add "${Server}" "${revzone}" "${IP2add}" PTR "${name}"."${domain}" "$KTYPE"
else
result4="$?"
continue
break
fi
fi
done
else
fi
continue
;;
fi
delete)
done
_KERBEROS
fi
;;
delete)
_KERBEROS
count=0
count=0
samba-tool dns delete ${Server} ${domain} "${name}" A ${ip} -k yes
$SAMBATOOL dns delete "${Server}" "${domain}" "${name}" A "${ip}" "$KTYPE"
result1="$?"
result1="$?"
# get existing reverse zones (if any)
# get existing reverse zones (if any)
ReverseZones=$(samba-tool dns zonelist ${Server} --reverse -k yes | grep 'pszZoneName' | awk '{print $NF}')
ReverseZones=$($SAMBATOOL dns zonelist "${Server}" --reverse "$KTYPE" | grep 'pszZoneName' | awk '{print $NF}')
if [ -z "$ReverseZones" ]; then
if [ -z "$ReverseZones" ]
then
logger "No reverse zone found, not updating"
logger "No reverse zone found, not updating"
result2='0'
result2='0'
count=$((count+1))
count=$((count+1))
else
else
for revzone in $ReverseZones
for revzone in $ReverseZones
do
do
rev_zone_info "$revzone" "${ip}"
rev_zone_info "$revzone" "${ip}"
if [[ ${ip} = $ZoneIP* ]] && [ "$ZoneIP" = "$RZIP" ]; then
if [[ ${ip} = $ZoneIP* ]] && [ "$ZoneIP" = "$RZIP" ]
host -t PTR ${ip} > /dev/null 2>&1
then
if [ "$?" -eq 0 ]; then
host -t PTR "${ip}" > /dev/null 2>&1
samba-tool dns delete ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes
ret="$?"
result2="$?"
if [ $ret -eq 0 ]
else
then
result2='0'
$SAMBATOOL dns delete "${Server}" "${revzone}" "${IP2add}" PTR "${name}"."${domain}" "$KTYPE"
count=$((count+1))
result2="$?"
fi
else
break
result2='0'
else
count=$((count+1))
continue
fi
fi
break
done
else
fi
continue
result3='0'
fi
result4='0'
done
;;
fi
*)
result3='0'
logger "Invalid action specified"
result4='0'
exit 103
;;
;;
*)
logger "Invalid action specified"
exit 103
;;
esac
esac
result="${result1}:${result2}:${result3}:${result4}"
result="${result1}:${result2}:${result3}:${result4}"
if [ "$count" -eq 0 ]; then
if [ "$count" -eq 0 ]
then
if [ "${result}" != "0:0:0:0" ]; then
if [ "${result}" != "0:0:0:0" ]
logger "DHCP-DNS $action failed: ${result}"
then
exit 1
logger "DHCP-DNS $action failed: ${result}"
else
exit 1
logger "DHCP-DNS $action succeeded"
else
fi
logger "DHCP-DNS $action succeeded"
fi
fi
fi
if [ "$Add_macAddress" != 'no' ]; then
if [ "$Add_macAddress" != 'no' ]
then
if [ -n "$DHCID" ]; then
if [ -n "$DHCID" ]
Computer_Object=$(ldbsearch -k yes -H ldap://"$Server" "(&(objectclass=computer)(objectclass=ieee802Device)(cn=$name))" | grep -v '#' | grep -v 'ref:')
then
if [ -z "$Computer_Object" ]; then
Computer_Object=$(ldbsearch "$KTYPE" -H ldap://"$Server" "(&(objectclass=computer)(objectclass=ieee802Device)(cn=$name))" | grep -v '#' | grep -v 'ref:')
# Computer object not found with the 'ieee802Device' objectclass, does the computer actually exist, it should.
if [ -z "$Computer_Object" ]
Computer_Object=$(ldbsearch -k yes -H ldap://"$Server" "(&(objectclass=computer)(cn=$name))" | grep -v '#' | grep -v 'ref:')
then
if [ -z "$Computer_Object" ]; then
# Computer object not found with the 'ieee802Device' objectclass, does the computer actually exist, it should.
logger "Computer '$name' not found. Exiting."
Computer_Object=$(ldbsearch "$KTYPE" -H ldap://"$Server" "(&(objectclass=computer)(cn=$name))" | grep -v '#' | grep -v 'ref:')
exit 68
if [ -z "$Computer_Object" ]
else
then
DN=$(echo "$Computer_Object" | grep 'dn:')
logger "Computer '$name' not found. Exiting."
objldif="$DN
exit 68
else
DN=$(echo "$Computer_Object" | grep 'dn:')
objldif="$DN
changetype: modify
changetype: modify
add: objectclass
add: objectclass
objectclass: ieee802Device"
objectclass: ieee802Device"
attrldif="$DN
attrldif="$DN
changetype: modify
changetype: modify
add: macAddress
add: macAddress
macAddress: $DHCID"
macAddress: $DHCID"
# add the ldif
# add the ldif
echo "$objldif" | ldbmodify -k yes -H ldap://"$Server"
echo "$objldif" | ldbmodify "$KTYPE" -H ldap://"$Server"
ret="$?"
ret="$?"
if [ "$ret" -ne 0 ]; then
if [ $ret -ne 0 ]
then
logger "Error modifying Computer objectclass $name in AD."
logger "Error modifying Computer objectclass $name in AD."
exit "${ret}"
exit "${ret}"
fi
fi
sleep 2
sleep 2
echo "$attrldif" | ldbmodify -k yes -H ldap://"$Server"
echo "$attrldif" | ldbmodify "$KTYPE" -H ldap://"$Server"
ret="$?"
ret="$?"
if [ "$ret" -ne 0 ]; then
if [ "$ret" -ne 0 ]; then
logger "Error modifying Computer attribute $name in AD."
logger "Error modifying Computer attribute $name in AD."
exit "${ret}"
exit "${ret}"
fi
fi
unset objldif
unset attrldif
unset objldif
unset attrldif
logger "Successfully modified Computer $name in AD"
logger "Successfully modified Computer $name in AD"
fi
fi
else
else
DN=$(echo "$Computer_Object" | grep 'dn:')
DN=$(echo "$Computer_Object" | grep 'dn:')
attrldif="$DN
attrldif="$DN
changetype: modify
changetype: modify
replace: macAddress
replace: macAddress
macAddress: $DHCID"
macAddress: $DHCID"
echo "$attrldif" | ldbmodify -k yes -H ldap://"$Server"
echo "$attrldif" | ldbmodify "$KTYPE" -H ldap://"$Server"
ret="$?"
ret="$?"
if [ "$ret" -ne 0 ]; then
if [ "$ret" -ne 0 ]
then
logger "Error modifying Computer attribute $name in AD."
logger "Error modifying Computer attribute $name in AD."
exit "${ret}"
exit "${ret}"
fi
fi
unset attrldif
unset attrldif
logger "Successfully modified Computer $name in AD"
logger "Successfully modified Computer $name in AD"
fi
fi
fi
fi
fi
fi
exit 0
exit 0





Line 440: Line 517:
Add_macAddress='no'
Add_macAddress='no'


It is near the top of the script. Change 'no' to 'yes'
It is near the top of the script. Change 'no' to 'yes'. Note you will need to grant DomainAdmin privileges to the DNS update user.




Line 487: Line 564:
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,6,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));
execute("/usr/local/bin/dhcp-dyndns.sh", "add", ClientIP, ClientDHCID, ClientName);
execute("/usr/local/bin/dhcp-dyndns.sh", "add", ClientIP, ClientDHCID, ClientName);
Line 576: Line 653:
}
}


Configure OMAPI and define a secret key.
== Configure OMAPI and Define a Secret Key ==

=== BIND 9.12 and earlier ===


Generate a random OMAPI key on the primary, using the dnssec-keygen utility distributed with BIND.
Generate a random OMAPI key on the primary, using the dnssec-keygen utility distributed with BIND.
Line 596: Line 675:
}
}


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

Continue with [[#All BIND versions]]

=== BIND 9.13 and later ===

Generate a random OMAPI key on either primary or secondary, using the tsig-keygen utility distributed with BIND.

tsig-keygen -a hmac-md5 omapi_key

The command will output text to your screen, similar to this:

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

Add the following lines to dhcpd.conf on both primary and secondary, followed by the text from the previous command:

omapi-port 7911;
omapi-key omapi_key;

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

Continue with [[#All BIND versions]]

=== All BIND versions ===


Restart both servers to apply the configuration changes.
Restart both servers to apply the configuration changes.

Latest revision as of 13:11, 11 August 2023

Introduction

This HowTo describes how to configure isc DHCP to update Samba dns records in AD.

It has now been tested with the Samba AD internal DNS server and BIND9_DLZ.

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.
  • You have created any required reverse zones.
  • You are logged into the DC as 'root'
  • If using Bind9, Bind9_dlz must be installed and working on the Samba AD DC that you are doing this on. Tested with various 9.x versions.


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

Or on FreeBSD:

# pkg install isc-dhcp44-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. On FreeBSD change /etc/dhcpduser.keytab to /usr/local/etc/dhcpduser.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
# On FreeBSD change the above line to #!/usr/local/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.6
#
# Copyright (C) Rowland Penny 2020-2022
#
# 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 may need to ensure that you have a useful path
# If you have 'path' problems, Uncomment the next line and adjust for
# your setup e.g. self-compiled Samba
#export PATH=/usr/local/samba/bin:/usr/local/samba/sbin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin

##########################################################################
#                                                                        #
#    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'
#                                                                        #
##########################################################################

# On FreeBSD change this to /usr/local/etc/dhcpduser.keytab
keytab=/etc/dhcpduser.keytab

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

_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
	ret="$?"
	if [ $ret -ne 0 ]
	then
		logger "${test} [dyndns] : Getting new ticket, old one has expired"
		# On FreeBSD change the -F to --no-forwardable
		kinit -F -k -t $keytab "${SETPRINCIPAL}"
		ret="$?"
		if [ $ret -ne 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
	rzoneip="${RevZone%.in-addr.arpa}"
	local rzonenum
	rzonenum=$(echo "$rzoneip" |  tr '.' '\n')
	declare -a words
	for n in $rzonenum
	do
		words+=("$n")
	done
	local numwords="${#words[@]}"

	unset ZoneIP
	unset RZIP
	unset IP2add

	case "$numwords" in
		1)
			# single ip rev zone '192'
			ZoneIP=$(echo "${IP}" | awk -F '.' '{print $1}')
			RZIP="${rzoneip}"
			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
}

BINDIR=$(samba -b | grep 'BINDIR' | grep -v 'SBINDIR' | awk '{print $NF}')
[[ -z $BINDIR ]] && printf "Cannot find the 'samba' binary, is it installed ?\\nOr is your path set correctly ?\\n"
WBINFO="$BINDIR/wbinfo"

SAMBATOOL=$(command -v samba-tool)
[[ -z $SAMBATOOL ]] && printf "Cannot find the 'samba-tool' binary, is it installed ?\\nOr is your path set correctly ?\\n"

MINVER=$($SAMBATOOL -V | grep -o '[0-9]*' | tr '\n' ' ' | awk '{print $2}')
if [ "$MINVER" -gt '14' ]
then
	KTYPE="--use-kerberos=required"
else
	KTYPE="-k yes"
fi

# 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="${domain^^}"

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

# Kerberos principal
SETPRINCIPAL="dhcpduser@${REALM}"
# Kerberos keytab as above
# 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 "$SAMBATOOL user create dhcpduser --random-password --description='Unprivileged user for DNS updates via ISC DHCP server'"
	logger "$SAMBATOOL user setexpiry dhcpduser --noexpiry"
	logger "$SAMBATOOL group addmembers DnsAdmins dhcpduser"
	exit 1
fi

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

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

# Exit if no ip address
if [ -z "${ip}" ]
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

# if you want computers with a hostname that starts with 'dhcp' in AD
# comment 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
		count=0
		# does host have an existing 'A' record ?
		mapfile -t A_REC < <($SAMBATOOL dns query "${Server}" "${domain}" "${name}" A "$KTYPE" 2>/dev/null | grep 'A:' | awk '{print $2}')
		if [ "${#A_REC[@]}" -eq 0 ]
		then
			# no A record to delete
			result1=0
			$SAMBATOOL dns add "${Server}" "${domain}" "${name}" A "${ip}" "$KTYPE"
			result2="$?"
		elif [ "${#A_REC[@]}" -gt 1 ]
		then
			for i in "${A_REC[@]}"
			do
				$SAMBATOOL dns delete "${Server}" "${domain}" "${name}" A "${i}" "$KTYPE"
			done
			# all A records deleted
			result1=0
			$SAMBATOOL dns add "${Server}" "${domain}" "${name}" A "${ip}" "$KTYPE"
			result2="$?"
		elif [ "${#A_REC[@]}" -eq 1 ]
		then
			# turn array into a variable
			VAR_A_REC="${A_REC[*]}"
			if [ "$VAR_A_REC" = "${ip}" ]
			then
				# Correct A record exists, do nothing
				logger "Correct 'A' record exists, not updating."
				result1=0
				result2=0
				count=$((count+1))
			elif [ "$VAR_A_REC" != "${ip}" ]
			then
				# Wrong A record exists
				logger "'A' record changed, updating record."
				$SAMBATOOL dns delete "${Server}" "${domain}" "${name}" A "${VAR_A_REC}" "$KTYPE"
				result1="$?"
				$SAMBATOOL dns add "${Server}" "${domain}" "${name}" A "${ip}" "$KTYPE"
				result2="$?"
			fi
		fi

		# get existing reverse zones (if any)
		ReverseZones=$($SAMBATOOL dns zonelist "${Server}" "$KTYPE" --reverse | grep 'pszZoneName' | awk '{print $NF}')
		if [ -z "$ReverseZones" ]; then
			logger "No reverse zone found, not updating"
			result3='0'
			result4='0'
			count=$((count+1))
		else
			for revzone in $ReverseZones
			do
				rev_zone_info "$revzone" "${ip}"
				if [[ ${ip} = $ZoneIP* ]] && [ "$ZoneIP" = "$RZIP" ]
				then
					# does host have an existing 'PTR' record ?
					PTR_REC=$($SAMBATOOL dns query "${Server}" "${revzone}" "${IP2add}" PTR "$KTYPE" 2>/dev/null | grep 'PTR:' | awk '{print $2}' | awk -F '.' '{print $1}')
					if [[ -z $PTR_REC ]]
					then
						# no PTR record to delete
						result3=0
						$SAMBATOOL dns add "${Server}" "${revzone}" "${IP2add}" PTR "${name}"."${domain}" "$KTYPE"
						result4="$?"
						break
					elif [ "$PTR_REC" = "${name}" ]
					then
						# Correct PTR record exists, do nothing
						logger "Correct 'PTR' record exists, not updating."
						result3=0
						result4=0
						count=$((count+1))
						break
					elif [ "$PTR_REC" != "${name}" ]
					then
						# Wrong PTR record exists
						# points to wrong host
						logger "'PTR' record changed, updating record."
						$SAMBATOOL dns delete "${Server}" "${revzone}" "${IP2add}" PTR "${PTR_REC}"."${domain}" "$KTYPE"
						result3="$?"
						$SAMBATOOL dns add "${Server}" "${revzone}" "${IP2add}" PTR "${name}"."${domain}" "$KTYPE"
						result4="$?"
						break
					fi
				else
					continue
				fi
			done
	        fi
		;;
	delete)
		_KERBEROS

		count=0
		$SAMBATOOL dns delete "${Server}" "${domain}" "${name}" A "${ip}" "$KTYPE"
		result1="$?"
		# get existing reverse zones (if any)
		ReverseZones=$($SAMBATOOL dns zonelist "${Server}" --reverse "$KTYPE" | grep 'pszZoneName' | awk '{print $NF}')
		if [ -z "$ReverseZones" ]
		then
			logger "No reverse zone found, not updating"
			result2='0'
			count=$((count+1))
		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
					ret="$?"
					if [ $ret -eq 0 ]
					then
						$SAMBATOOL dns delete "${Server}" "${revzone}" "${IP2add}" PTR "${name}"."${domain}" "$KTYPE"
						result2="$?"
					else
						result2='0'
						count=$((count+1))
					fi
					break
				else
					continue
				fi
			done
		fi
		result3='0'
		result4='0'
		;;
	*)
		logger "Invalid action specified"
		exit 103
	;;
esac

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

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

if [ "$Add_macAddress" != 'no' ]
then
	if [ -n "$DHCID" ]
	then
		Computer_Object=$(ldbsearch "$KTYPE" -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 "$KTYPE" -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 "$KTYPE" -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 "$KTYPE" -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 "$KTYPE" -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
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'. Note you will need to grant DomainAdmin privileges to the DNS update user.


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 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
# however the dhcp update script will obtain the short hostname.
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

BIND 9.12 and earlier

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.

Continue with #All BIND versions

BIND 9.13 and later

Generate a random OMAPI key on either primary or secondary, using the tsig-keygen utility distributed with BIND.

tsig-keygen -a hmac-md5 omapi_key

The command will output text to your screen, similar to this:

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

Add the following lines to dhcpd.conf on both primary and secondary, followed by the text from the previous command:

omapi-port 7911;
omapi-key omapi_key;
key "omapi_key" {
    algorithm hmac-md5;
    secret "some_secret_text";
};

Continue with #All BIND versions

All BIND versions

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.