#!/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. # # Modifications: Updates for faster local access to databases and compatibility with freebsds samba # # Version: 0.9.2 # # Copyright (C) Rowland Penny 2020 # Extended by Robin Freund 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 . ########################################################################## # # # 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='yes' # # ########################################################################## 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" if [[ "$OSTYPE" == "freebsd"* ]]; then kinit --no-forwardable -k -t /etc/dhcpduser.keytab "${SETPRINCIPAL}" else kinit -F -k -t /etc/dhcpduser.keytab "${SETPRINCIPAL}" fi 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 '178.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" LDBCOMMAND="ldbsearch" if [[ "$OSTYPE" == "freebsd"* ]]; then LDBCOMMAND="samba-ldbsearch" fi LDBCOMMANDMOD="ldbmodify" if [[ "$OSTYPE" == "freebsd"* ]]; then LDBCOMMANDMOD="samba-ldbmodify" 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=$(echo ${domain^^}) # Samba SAM file - can also be ldap://${SERVER}, but that could get very slow if [[ "$OSTYPE" == "freebsd"* ]]; then DATABASE="/var/db/samba4/private/sam.ldb" else DATABASE="/var/lib/samba/private/sam.ldb" fi # 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 if [[ ${ip} = $A_REC ]]; then logger "Existing A record of ${name} matches new IP, not updating." result1="0" result2="0" else samba-tool dns delete ${Server} ${domain} "${name}" A ${ip} -k yes result1="$?" logger "Deleted existing host A record for ${name}" samba-tool dns add ${Server} ${domain} "${name}" A ${ip} -k yes result2="$?" logger "Added new A record" fi else result1=0 logger "No A record yet" samba-tool dns add ${Server} ${domain} "${name}" A ${ip} -k yes result2="$?" logger "Added new A record" fi # get ptr record for possible early exit PTR_REC=$(host -t PTR "${ip}" | awk '{print $NF}') if [[ $PTR_REC = "${name}".${domain}. ]]; then logger "Existing PTR record of ${ip} matches new hostname, not updating." result3='0' result4='0' else # 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 logger "Looking in zone ${revzone}.${domain}" rev_zone_info "$revzone" "${ip}" if [[ ${ip} = $ZoneIP* ]] && [ "$ZoneIP" = "$RZIP" ]; then if [[ $PTR_REC == *.${domain}. ]]; then samba-tool dns delete ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes result3="$?" logger "Deleted existing host PTR record for {ip}" samba-tool dns add ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes result4="$?" logger "Added new PTR record" else result3='0' logger "No PTR record for ${ip} yet - found $PTR_REC for ${domain}" samba-tool dns add ${Server} ${revzone} ${IP2add} PTR "${name}".${domain} -k yes result4="$?" logger "Added new PTR record" fi break else continue fi done fi 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 logger "Searching for $name in AD.." Computer_Object=$(ldbsearch -k yes -H "$DATABASE" "(&(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 "$DATABASE" "(&(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 "$DATABASE" 2&>1 | logger & 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 "$DATABASE" 2&>1 | logger & 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" 2&>1 | logger & 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