Difference between revisions of "Configure DHCP to update DNS records with BIND9"

m (/* fixed major oops in script)
(Add notes for FreeBSD and update script to work on it.)
 
(8 intermediate revisions by one other user 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 17: Line 17:
 
* Bind9_dlz is installed and working on the Samba AD DC, tested with various 9.x versions.
 
* Bind9_dlz is installed and working on the Samba AD DC, tested with various 9.x versions.
  
* You have created the reverse zone.
+
* You have created any required reverse zones.
  
 
* You are logged into the DC as 'root'
 
* You are logged into the DC as 'root'
Line 46: Line 46:
 
  # 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 60:
 
  # 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 67:
 
{{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 76:
  
 
  #!/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.0
+
  # Version: 0.9.3
 
  #
 
  #
  # Copyright (C) Rowland Penny 2020
+
  # Copyright (C) Rowland Penny 2020-2021
 
  #
 
  #
 
  # 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 99:
 
  # 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/>.
 +
 +
# Make sure we have a useful path
 +
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin
 
   
 
   
 
  ##########################################################################
 
  ##########################################################################
Line 106: Line 111:
 
  #                                                                        #
 
  #                                                                        #
 
  ##########################################################################
 
  ##########################################################################
 +
 +
# On FreeBSD change this to /usr/local/etc/dhcpduser.keytab
 +
keytab=/etc/dhcpduser.keytab
 
   
 
   
 
  usage() {
 
  usage() {
Line 124: Line 132:
 
  if [ "$?" != "0" ]; then
 
  if [ "$?" != "0" ]; then
 
     logger "${test} [dyndns] : Getting new ticket, old one has expired"
 
     logger "${test} [dyndns] : Getting new ticket, old one has expired"
     kinit -F -k -t /etc/dhcpduser.keytab "${SETPRINCIPAL}"
+
     kinit -F -k -t $keytab "${SETPRINCIPAL}"
 +
    # On FreeBSD change the -F to --no-forwardable   
 
     if [ "$?" != "0" ]; then
 
     if [ "$?" != "0" ]; then
 
         logger "${test} [dyndns] : dhcpd kinit for dynamic DNS failed"
 
         logger "${test} [dyndns] : dhcpd kinit for dynamic DNS failed"
         exit 1;
+
         exit 1
    fi
+
    fi
 
  fi
 
  fi
 
  }
 
  }
Line 135: Line 144:
 
     local RevZone="$1"
 
     local RevZone="$1"
 
     local IP="$2"
 
     local IP="$2"
     local rzoneip=$(echo "$RevZone" | sed 's/\.in-addr.arpa//')
+
     local rzoneip
     local rzonenum=$(echo "$rzoneip" | sed 's/\./ /g')
+
    rzoneip=$(echo "$RevZone" | sed 's/\.in-addr.arpa//')
     local words=($rzonenum)
+
     local rzonenum
 +
    rzonenum=$(echo "$rzoneip" | tr '.' '\n')
 +
     declare -a words
 +
    for n in $rzonenum
 +
    do
 +
      words+=("$n")
 +
    done
 
     local numwords="${#words[@]}"
 
     local numwords="${#words[@]}"
 +
 +
    unset ZoneIP
 +
    unset RZIP
 +
    unset IP2add
 +
 
     case "$numwords" in
 
     case "$numwords" in
 
         1) # single ip rev zone '192'
 
         1) # single ip rev zone '192'
 
             ZoneIP=$(echo "${IP}" | awk -F '.' '{print $1}')
 
             ZoneIP=$(echo "${IP}" | awk -F '.' '{print $1}')
             RZIP=$(echo "${rzoneip}" | awk -F '.' '{print $3}')
+
             RZIP="${rzoneip}"
 
             IP2add=$(echo "${IP}" | awk -F '.' '{print $4"."$3"."$2}')
 
             IP2add=$(echo "${IP}" | awk -F '.' '{print $4"."$3"."$2}')
 
             ;;
 
             ;;
Line 159: Line 179:
 
             ;;
 
             ;;
 
     esac
 
     esac
    echo "$ZoneIP"
+
}
    echo "$RZIP"
 
    echo "$IP2add"
 
 
   
 
   
}
 
 
 
  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"
Line 186: Line 202:
 
  # 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')"
Line 201: Line 217:
 
  # Check for Kerberos keytab
 
  # Check for Kerberos keytab
 
  if [ ! -f /etc/dhcpduser.keytab ]; then
 
  if [ ! -f /etc/dhcpduser.keytab ]; then
     echo "Required keytab /etc/dhcpduser.keytab not found, it needs to be created."
+
     logger "Required keytab $keytab not found, it needs to be created."
     echo "Use the following commands as root"
+
     logger "Use the following commands as root"
     echo "samba-tool domain exportkeytab --principal=${SETPRINCIPAL} /etc/dhcpduser.keytab"
+
     logger "samba-tool domain exportkeytab --principal=${SETPRINCIPAL} $keytab"
     echo "chown XXXX:XXXX /etc/dhcpduser.keytab"
+
     logger "chown XXXX:XXXX $keytab"
     echo "Replace 'XXXX:XXXX' with the user & group that dhcpd runs as on your distro"
+
     logger "Replace 'XXXX:XXXX' with the user & group that dhcpd runs as on your distro"
     echo "chmod 400 /etc/dhcpduser.keytab"
+
     logger "chmod 400 $keytab"
 
     exit 1
 
     exit 1
 
  fi
 
  fi
Line 217: Line 233:
 
   
 
   
 
  # Exit if no ip address or mac-address
 
  # Exit if no ip address or mac-address
  if [ -z "${ip}" ] || [ -z "${DHCID}" ]; then
+
  if [ -z "${ip}" ]; then
 
     usage
 
     usage
 
     exit 1
 
     exit 1
Line 228: Line 244:
 
     else
 
     else
 
         usage
 
         usage
         exit 1;
+
         exit 1
 
     fi
 
     fi
 
  fi
 
  fi
Line 237: Line 253:
 
           exit
 
           exit
 
           ;;
 
           ;;
  #  * ) : ;;
 
 
  esac
 
  esac
 
   
 
   
Line 243: Line 258:
 
  # if you do not want computers without a hostname in AD
 
  # if you do not want computers without a hostname in AD
 
  # uncomment the following block of code.
 
  # uncomment the following block of code.
  <nowiki>if [[ $name == dhcp* ]]; then</nowiki>
+
  #<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 ##
Line 252: Line 267:
 
     add)
 
     add)
 
         _KERBEROS
 
         _KERBEROS
+
        count=0
         samba-tool dns delete ${Server} ${domain} "${name}" A ${ip} -k yes
+
        # does host have an existing 'A' record ?
         result1="$?"
+
         A_REC=$(samba-tool dns query ${Server} ${domain} ${name} A -k yes 2>/dev/null | grep 'A:' | awk '{print $2}')
        samba-tool dns add ${Server} ${domain} "${name}" A ${ip} -k yes
+
        <nowiki>if [[ -z $A_REC ]]; then</nowiki>
        result2="$?"
+
            # no A record to delete
 +
            result1=0
 +
            samba-tool dns add ${Server} ${domain} "${name}" A ${ip} -k yes
 +
            result2="$?"
 +
         elif [ "$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 [ "$A_REC" != "${ip}" ]; then
 +
              # Wrong A record exists
 +
              logger "'A' record changed, updating record."
 +
              samba-tool dns delete ${Server} ${domain} "${name}" A ${A_REC} -k yes
 +
              result1="$?"
 +
              samba-tool dns add ${Server} ${domain} "${name}" A ${ip} -k yes
 +
              result2="$?"
 +
        fi
 
   
 
   
 
         # get existing reverse zones (if any)
 
         # get existing reverse zones (if any)
         ReverseZones=$(samba-tool dns zonelist ${Server} --reverse | grep 'pszZoneName' | awk '{print $NF}')
+
         ReverseZones=$(samba-tool dns zonelist ${Server} -k yes --reverse | grep 'pszZoneName' | awk '{print $NF}')
 
         if [ -z "$ReverseZones" ]; then
 
         if [ -z "$ReverseZones" ]; then
             echo "No reverse zone found, not updating"
+
             logger "No reverse zone found, not updating"
 
             result3='0'
 
             result3='0'
 
             result4='0'
 
             result4='0'
 +
            count=$((count+1))
 
         else
 
         else
 
             for revzone in $ReverseZones
 
             for revzone in $ReverseZones
Line 269: Line 302:
 
               rev_zone_info "$revzone" "${ip}"
 
               rev_zone_info "$revzone" "${ip}"
 
               if [[ ${ip} = $ZoneIP* ]] && [ "$ZoneIP" = "$RZIP" ]; then
 
               if [[ ${ip} = $ZoneIP* ]] && [ "$ZoneIP" = "$RZIP" ]; then
                   host -t PTR ${ip} > /dev/null 2>&1
+
                   # does host have an existing 'PTR' record ?
                   if [ "$?" -eq 0 ]; then
+
                  PTR_REC=$(samba-tool dns query ${Server} ${revzone} ${IP2add} PTR -k yes 2>/dev/null | grep 'PTR:' | awk '{print $2}' | awk -F '.' '{print $1}')
                       samba-tool dns delete ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes
+
                   <nowiki>if [[ -z $PTR_REC ]]; then</nowiki>
                       result3="$?"
+
                      # no PTR record to delete
                   else
+
                      result3=0
                      result3='0'
+
                       samba-tool dns add ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes
 +
                       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."
 +
                        samba-tool dns delete ${Server} ${revzone} ${IP2add} PTR "${PTR_REC}".${domain} -k yes
 +
                        result3="$?"
 +
                        samba-tool dns add ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes
 +
                        result4="$?"
 +
                        break
 
                   fi
 
                   fi
                  samba-tool dns add ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes
 
                  result4="$?"
 
                  break
 
 
               else
 
               else
 
                   continue
 
                   continue
Line 288: Line 336:
 
         _KERBEROS
 
         _KERBEROS
 
   
 
   
 +
        count=0
 
         samba-tool dns delete ${Server} ${domain} "${name}" A ${ip} -k yes
 
         samba-tool dns delete ${Server} ${domain} "${name}" A ${ip} -k yes
 
         result1="$?"
 
         result1="$?"
 
         # get existing reverse zones (if any)
 
         # get existing reverse zones (if any)
         ReverseZones=$(samba-tool dns zonelist ${Server} --reverse | grep 'pszZoneName' | awk '{print $NF}')
+
         ReverseZones=$(samba-tool dns zonelist ${Server} --reverse -k yes | 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))
 
         else
 
         else
 
             for revzone in $ReverseZones
 
             for revzone in $ReverseZones
Line 306: Line 356:
 
                   else
 
                   else
 
                       result2='0'
 
                       result2='0'
 +
                      count=$((count+1))
 
                   fi
 
                   fi
 
                   break
 
                   break
Line 324: Line 375:
 
  result="${result1}:${result2}:${result3}:${result4}"
 
  result="${result1}:${result2}:${result3}:${result4}"
 
   
 
   
  if [ "${result}" != "0:0:0:0" ]; then
+
  if [ "$count" -eq 0 ]; then
    logger "DHCP-DNS Update failed: ${result}"
+
    if [ "${result}" != "0:0:0:0" ]; then
    exit 1
+
        logger "DHCP-DNS $action failed: ${result}"
else
+
        exit 1
    logger "DHCP-DNS Update succeeded"
+
    else
 +
        logger "DHCP-DNS $action succeeded"
 +
    fi
 
  fi
 
  fi
 
   
 
   
 
  if [ "$Add_macAddress" != 'no' ]; then
 
  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 [ -n "$DHCID" ]; then
     if [ -z "$Computer_Object" ]; then
+
         Computer_Object=$(ldbsearch -k yes -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.
 
         Computer_Object=$(ldbsearch -k yes -H ldap://"$Server" "(&(objectclass=computer)(cn=$name))" | grep -v '#' | grep -v 'ref:')
 
 
         if [ -z "$Computer_Object" ]; then
 
         if [ -z "$Computer_Object" ]; then
             logger "Computer '$name' not found. Exiting."
+
             # Computer object not found with the 'ieee802Device' objectclass, does the computer actually exist, it should.
            exit 68
+
            Computer_Object=$(ldbsearch -k yes -H ldap://"$Server" "(&(objectclass=computer)(cn=$name))" | grep -v '#' | grep -v 'ref:')
        else
+
            if [ -z "$Computer_Object" ]; 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 -k yes -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
 +
                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
 
             fi
             sleep 2
+
        else
 +
             DN=$(echo "$Computer_Object" | grep 'dn:')
 +
            attrldif="$DN
 +
changetype: modify
 +
replace: macAddress
 +
macAddress: $DHCID"
 +
 
             echo "$attrldif" | ldbmodify -k yes -H ldap://"$Server"
 
             echo "$attrldif" | ldbmodify -k yes -H ldap://"$Server"
 
             ret="$?"
 
             ret="$?"
Line 365: Line 436:
 
                 exit "${ret}"
 
                 exit "${ret}"
 
             fi
 
             fi
            unset objldif
 
 
             unset attrldif
 
             unset attrldif
 
             logger "Successfully modified Computer $name in AD"
 
             logger "Successfully modified Computer $name in AD"
 
         fi
 
         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
 
  fi
 
  fi
 +
 
  exit 0
 
  exit 0
 +
  
 
If you wish to store the computers MAC address in AD, find this line:
 
If you wish to store the computers MAC address in AD, find this line:
Line 392: Line 449:
 
  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 421: Line 478:
 
   option domain-name "samdom.example.com";
 
   option domain-name "samdom.example.com";
 
   option domain-name-servers 192.168.0.6, 192.168.0.5;
 
   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;
 
   option ntp-servers 192.168.0.5, 192.168.0.6;
 
   pool {
 
   pool {
Line 464: Line 520:
 
  log(concat("Expired: IP: ", ClientIP));
 
  log(concat("Expired: IP: ", ClientIP));
 
  # cannot get a ClientName here, for some reason that always fails
 
  # 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");
 
  execute("/usr/local/bin/dhcp-dyndns.sh", "delete", ClientIP, "", "0");
 
  }
 
  }
Line 490: Line 547:
 
   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 563: Line 620:
 
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 605: Line 662:
  
 
Any questions or problems, ask on the Samba mailing list.
 
Any questions or problems, ask on the Samba mailing list.
 +
{{Imbox
 +
| type = note
 +
| text = Do not log a bug report on Samba bugzilla for any problems you have with this set up, ask on the samba mailing list.
 +
}}
  
  

Latest revision as of 07:49, 20 February 2021

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.
  • 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 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

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.3
#
# Copyright (C) Rowland Penny 2020-2021
#
# 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/>.

# Make sure we have a useful path
export PATH=/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() {
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 $keytab "${SETPRINCIPAL}"
    # On FreeBSD change the -F to --no-forwardable     
    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
    rzoneip=$(echo "$RevZone" | sed 's/\.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}')
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 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 "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
    logger "Required keytab $keytab not found, it needs to be created."
    logger "Use the following commands as root"
    logger "samba-tool 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 or mac-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

# 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
        count=0
        # 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}')
        if [[ -z $A_REC ]]; then
            # no A record to delete
            result1=0
            samba-tool dns add ${Server} ${domain} "${name}" A ${ip} -k yes
            result2="$?"
        elif [ "$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 [ "$A_REC" != "${ip}" ]; then
              # Wrong A record exists
              logger "'A' record changed, updating record."
              samba-tool dns delete ${Server} ${domain} "${name}" A ${A_REC} -k yes
              result1="$?"
              samba-tool dns add ${Server} ${domain} "${name}" A ${ip} -k yes
              result2="$?"
        fi

        # get existing reverse zones (if any)
        ReverseZones=$(samba-tool dns zonelist ${Server} -k yes --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=$(samba-tool dns query ${Server} ${revzone} ${IP2add} PTR -k yes 2>/dev/null | grep 'PTR:' | awk '{print $2}' | awk -F '.' '{print $1}')
                  if [[ -z $PTR_REC ]]; then
                      # no PTR record to delete
                      result3=0
                      samba-tool dns add ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes
                      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."
                        samba-tool dns delete ${Server} ${revzone} ${IP2add} PTR "${PTR_REC}".${domain} -k yes
                        result3="$?"
                        samba-tool dns add ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes
                        result4="$?"
                        break
                  fi
              else
                  continue
              fi
            done
        fi
        ;;
 delete)
        _KERBEROS

        count=0
        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 -k yes | 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
                  if [ "$?" -eq 0 ]; then
                      samba-tool dns delete ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes
                      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 -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
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.

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.