510 lines
14 KiB
Bash
510 lines
14 KiB
Bash
|
#!/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.5
|
||
|
#
|
||
|
# 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/>.
|
||
|
|
||
|
# Revised 2022-10-22, Rohhie, as follows:
|
||
|
# - ログをstderrに出力するようにした。
|
||
|
# - サーバー名とドメイン名、DNS管理者ユーザー名を環境変数で指定するようにした。
|
||
|
# - ユーザーは作ってある前提なのでテストロジックを削除した。
|
||
|
# - klistの-s指定をファイル名の前に移動した。
|
||
|
# - 期限切れで削除になる時、対象IPアドレスからホスト名を取得するようにした。
|
||
|
# - IPv6に対応する処理を追加した(ホームラボで使いたい範囲で動く程度)。
|
||
|
|
||
|
# 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 '$DHCPDNSADM' 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/<USERNAME>.keytab
|
||
|
keytab=/etc/$DHCPDNSADM.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 -s "${test} [dyndns] : Running check for valid kerberos ticket"
|
||
|
klist -c -s "${KRB5CCNAME}"
|
||
|
ret="$?"
|
||
|
if [ $ret -ne 0 ]
|
||
|
then
|
||
|
logger -s "${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 -s "${test} [dyndns] : dhcpd kinit for dynamic DNS failed"
|
||
|
exit 1
|
||
|
fi
|
||
|
fi
|
||
|
}
|
||
|
|
||
|
rev_zone_info()
|
||
|
{
|
||
|
local RevZone="$1"
|
||
|
local IP="$2"
|
||
|
local rzoneip
|
||
|
local rzonenum
|
||
|
|
||
|
if [[ $RevZone =~ .+"in-addr.arpa"$ ]]; then
|
||
|
if [ $3 = "AAAA" ]; then
|
||
|
ZoneIP="-no match-"
|
||
|
return;
|
||
|
fi
|
||
|
rzoneip="${RevZone%.in-addr.arpa}"
|
||
|
else
|
||
|
if [ $3 = "A" ]; then
|
||
|
ZoneIP="-no match-"
|
||
|
return;
|
||
|
fi
|
||
|
rzoneip="${RevZone%.ip6.arpa}"
|
||
|
fi
|
||
|
|
||
|
unset ZoneIP
|
||
|
unset RZIP
|
||
|
unset IP2add
|
||
|
|
||
|
if [ $3 = "A" ]; then
|
||
|
rzonenum=$(echo "$rzoneip" | tr '.' '\n')
|
||
|
declare -a words
|
||
|
for n in $rzonenum
|
||
|
do
|
||
|
words+=("$n")
|
||
|
done
|
||
|
local numwords="${#words[@]}"
|
||
|
|
||
|
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
|
||
|
else
|
||
|
local v6ip=$(echo -n $2 | sed "s/://g")
|
||
|
local v6zn=$(echo -n $rzoneip | sed "s/\.//g" | rev)
|
||
|
if [[ $v6ip =~ "$v6zn".+ ]]; then
|
||
|
ZoneIP=$2
|
||
|
RZIP="${ZoneIP}"
|
||
|
IP2add=$(echo -n $v6ip | sed "s/$v6zn\(.\+\)/\1/" | rev | sed "s/\(.\)/\1./g" | sed "s/.$//")
|
||
|
rzoneip="${RevZone%.ip6.arpa}"
|
||
|
else
|
||
|
ZoneIP="-no match-"
|
||
|
return;
|
||
|
fi
|
||
|
fi
|
||
|
}
|
||
|
|
||
|
ipv6_short_to_long () {
|
||
|
divaddr=$(echo -n $1 | sed "s/::/:-:/")
|
||
|
local IFSBAK=$IFS
|
||
|
IFS=: divaddr=($divaddr)
|
||
|
IFS=$IFSBAK
|
||
|
|
||
|
defaddr=()
|
||
|
for element in "${divaddr[@]}"; do
|
||
|
if [ $element = "-" ]; then
|
||
|
padmax=$((8-${#divaddr[@]}))
|
||
|
for (( i=0; i<=$padmax; i++ ))
|
||
|
do
|
||
|
defaddr+=("0000")
|
||
|
done
|
||
|
else
|
||
|
padding="0000${element}"
|
||
|
defaddr+=(${padding: -4})
|
||
|
fi
|
||
|
done
|
||
|
|
||
|
longaddr=$(echo -n ${defaddr[@]} | sed "s/ /:/g")
|
||
|
|
||
|
echo $longaddr
|
||
|
}
|
||
|
|
||
|
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)
|
||
|
Server=${DHCPSERVER}.${DHCPDOMAIN}
|
||
|
|
||
|
# DNS domain
|
||
|
#domain=$(hostname -d)
|
||
|
domain=${DHCPDOMAIN}
|
||
|
if [ -z "${domain}" ]
|
||
|
then
|
||
|
logger -s "Cannot obtain domain name, is DNS set up correctly?"
|
||
|
logger -s "Cannot continue... Exiting."
|
||
|
exit 1
|
||
|
fi
|
||
|
|
||
|
# Samba realm
|
||
|
REALM="${domain^^}"
|
||
|
|
||
|
# krbcc ticket cache
|
||
|
export KRB5CCNAME="/tmp/dhcp-dyndns.cc"
|
||
|
|
||
|
# Kerberos principal
|
||
|
SETPRINCIPAL="${DHCPDNSADM}@${REALM}"
|
||
|
# Kerberos keytab as above
|
||
|
# krbcc ticket cache : /tmp/dhcp-dyndns.cc
|
||
|
#TESTUSER="$($WBINFO -u | grep $DHCPDNSADM)"
|
||
|
#if [ -z "${TESTUSER}" ]
|
||
|
#then
|
||
|
# logger -s "No AD dhcp user exists, need to create it first.. exiting."
|
||
|
# logger -s "you can do this by typing the following commands"
|
||
|
# logger -s "kinit Administrator@${REALM}"
|
||
|
# logger -s "$SAMBATOOL user create $DHCPDNSADM --random-password --description='Unprivileged user for DNS updates via ISC DHCP server'"
|
||
|
# logger -s "$SAMBATOOL user setexpiry $DHCPDNSADM --noexpiry"
|
||
|
# logger -s "$SAMBATOOL group addmembers DnsAdmins $DHCPDNSADM"
|
||
|
# exit 1
|
||
|
#fi
|
||
|
|
||
|
# Check for Kerberos keytab
|
||
|
if [ ! -f /etc/$DHCPDNSADM.keytab ]
|
||
|
then
|
||
|
logger -s "Required keytab $keytab not found, it needs to be created."
|
||
|
logger -s "Use the following commands as root"
|
||
|
logger -s "$SAMBATOOL domain exportkeytab --principal=${SETPRINCIPAL} $keytab"
|
||
|
logger -s "chown XXXX:XXXX $keytab"
|
||
|
logger -s "Replace 'XXXX:XXXX' with the user & group that dhcpd runs as on your distro"
|
||
|
logger -s "chmod 400 $keytab"
|
||
|
exit 1
|
||
|
fi
|
||
|
|
||
|
# Variables supplied by dhcpd.conf
|
||
|
action="$1"
|
||
|
DHCID="$3"
|
||
|
name="${4%%.*}"
|
||
|
if [ "$5" = "-IPv6-" ]; then
|
||
|
ip=$(ipv6_short_to_long $2)
|
||
|
AorAAAA="AAAA"
|
||
|
else
|
||
|
ip="$2"
|
||
|
AorAAAA="A"
|
||
|
fi
|
||
|
|
||
|
# 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 -s "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 -s "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=$($SAMBATOOL dns query "${Server}" "${domain}" "${name}" $AorAAAA "$KTYPE" 2>/dev/null | grep "$AorAAAA:" | awk '{print $2}')
|
||
|
# turn A_REC into an array
|
||
|
A_REC=("$A_REC")
|
||
|
if [ "${#A_REC[@]}" -eq 0 ]
|
||
|
then
|
||
|
# no A record to delete
|
||
|
result1=0
|
||
|
$SAMBATOOL dns add "${Server}" "${domain}" "${name}" $AorAAAA "${ip}" "$KTYPE"
|
||
|
result2="$?"
|
||
|
elif [ "${#A_REC[@]}" -gt 1 ]
|
||
|
then
|
||
|
for i in "${A_REC[@]}"
|
||
|
do
|
||
|
$SAMBATOOL dns delete "${Server}" "${domain}" "${name}" $AorAAAA "${i}" "$KTYPE"
|
||
|
done
|
||
|
# all A records deleted
|
||
|
result1=0
|
||
|
$SAMBATOOL dns add "${Server}" "${domain}" "${name}" $AorAAAA "${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 -s "Correct '$AorAAAA' record exists, not updating."
|
||
|
result1=0
|
||
|
result2=0
|
||
|
count=$((count+1))
|
||
|
elif [ "$VAR_A_REC" != "${ip}" ]
|
||
|
then
|
||
|
# Wrong A record exists
|
||
|
logger -s "'$AorAAAA' record changed, updating record."
|
||
|
$SAMBATOOL dns delete "${Server}" "${domain}" "${name}" $AorAAAA "${VAR_A_REC}" "$KTYPE"
|
||
|
result1="$?"
|
||
|
$SAMBATOOL dns add "${Server}" "${domain}" "${name}" $AorAAAA "${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 -s "No reverse zone found, not updating"
|
||
|
result3='0'
|
||
|
result4='0'
|
||
|
count=$((count+1))
|
||
|
else
|
||
|
for revzone in $ReverseZones
|
||
|
do
|
||
|
rev_zone_info "$revzone" "${ip}" "${AorAAAA}"
|
||
|
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 -s "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 -s "'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
|
||
|
name=$(host -t PTR "${ip}" | sed 's/.\+pointer \([^.]\+\).\+$/\1/')
|
||
|
$SAMBATOOL dns delete "${Server}" "${domain}" "${name}" $AorAAAA "${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 -s "No reverse zone found, not updating"
|
||
|
result2='0'
|
||
|
count=$((count+1))
|
||
|
else
|
||
|
for revzone in $ReverseZones
|
||
|
do
|
||
|
rev_zone_info "$revzone" "${ip}" "${AorAAAA}"
|
||
|
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 -s "Invalid action specified"
|
||
|
exit 103
|
||
|
;;
|
||
|
esac
|
||
|
|
||
|
result="${result1}:${result2}:${result3}:${result4}"
|
||
|
|
||
|
if [ "$count" -eq 0 ]
|
||
|
then
|
||
|
if [ "${result}" != "0:0:0:0" ]
|
||
|
then
|
||
|
logger -s "DHCP-DNS $action failed: ${result}"
|
||
|
exit 1
|
||
|
else
|
||
|
logger -s "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 -s "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 -s "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 -s "Error modifying Computer attribute $name in AD."
|
||
|
exit "${ret}"
|
||
|
fi
|
||
|
unset objldif
|
||
|
unset attrldif
|
||
|
logger -s "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 -s "Error modifying Computer attribute $name in AD."
|
||
|
exit "${ret}"
|
||
|
fi
|
||
|
unset attrldif
|
||
|
logger -s "Successfully modified Computer $name in AD"
|
||
|
fi
|
||
|
fi
|
||
|
fi
|
||
|
|
||
|
exit 0
|