Product SiteDocumentation Site

Глава 53. Настройка DHCP-сервера для обновления DNS-записей

53.1. Настройка DHCP-сервера
53.2. Настройка переключения DHCP
В этом разделе описана настройка DHCP-сервера для автоматического обновления DNS-записей Samba в домене.
Предварительные условия:
  • DHCP-сервер устанавливается на одном из контроллеров домена;
  • созданы все необходимые обратные зоны;
  • если используется Bind9, Bind9_dlz должен быть установлен и должен работать на контроллере домена, на котором выполняется данная настройка.
Описание имеющейся сети:
  • Realm: TEST.ALT
  • Подсеть: 192.168.0.0
  • Маска сети: 255.255.255.0
  • Широковещательный адрес: 192.168.0.255
  • Шлюз по умолчанию: 192.168.0.1
  • Имя домена: test.alt
  • DNS-сервера: 192.168.0.132, 192.168.0.133
  • Netbios-сервера: 192.168.0.132, 192.168.0.133
  • Ntp-сервера: 192.168.0.132, 192.168.0.133
  • Диапазон арендуемых IP-адресов: 192.168.0.150 192.168.0.200

53.1. Настройка DHCP-сервера

Все действия, указанные ниже, выполняются на узле dc1.test.alt (192.168.0.132), если не указано иное.
Создать пользователя (в примере dhcpduser), от имени которого будут производится обновления DNS-записей:
# samba-tool user create dhcpduser \
  --description="Пользователь для обновления TSIG-GSSAPI DNS через DHCP-сервер" \
  --random-password
User 'dhcpduser' added successfully
Установить срок действия пароля (бессрочный) для созданного пользователя и добавить его в группу DnsAdmins:
# samba-tool user setexpiry dhcpduser --noexpiry
Expiry for user 'dhcpduser' disabled.
# samba-tool group addmembers DnsAdmins dhcpduser
Added members to group DnsAdmins
Экспортировать файл keytab, чтобы пользователь мог аутентифицироваться через Kerberos:
# samba-tool domain exportkeytab --principal=dhcpduser@TEST.ALT /etc/dhcp/dhcpduser.keytab
Export one principal to /etc/dhcp/dhcpduser.keytab
# chown dhcpd:dhcp /etc/dhcp/dhcpduser.keytab
# chmod 400 /etc/dhcp/dhcpduser.keytab

Примечание

Параметр dhcpd:dhcp указывает пользователя и группу, от имени которых работает DHCP-сервер.
Создать скрипт, который будет выполнять обновления (файл /usr/local/bin/dhcp-dyndns.sh):
#!/bin/bash
#
# This script is for secure DDNS updates on Samba,
# it can also add the 'macAddress' to the Computers object.
#
# Version: 0.9.6
#

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

keytab=/etc/dhcp/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"
		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

Примечание

Если нужно сохранять MAC-адреса узлов в AD, следует заменить строку:
Add_macAddress='no'
на:
Add_macAddress='yes'
Следует обратить внимание, что необходимо предоставить права администратора домена пользователю обновления DNS.
Установить права для скрипта:
# chmod 755 /usr/local/bin/dhcp-dyndns.sh
Создать резервную копию исходного файла конфигурации:
# cp /etc/dhcp/dhcpd.conf /etc/dhcp/dhcpd.conf.orig
Внести изменения в файл конфигурации /etc/dhcp/dhcpd.conf:
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-servers 192.168.0.132, 192.168.0.133;
        option ntp-servers 192.168.0.132, 192.168.0.133;
        option domain-name "test.alt";
        default-lease-time 3600;
        pool {
             max-lease-time 1800; #30 минут
             range 192.168.0.150 192.168.0.200;
             }
}

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");
}
Отключить chroot для DHCP-сервера:
# control dhcpd-chroot disabled
Перезапустить DHCP-сервер:
# systemctl restart dhcpd
Теперь если на клиенте изменить сетевую конфигурацию со статического IP-адреса, на получение IP-адреса от DHCP-сервера, в журнале на сервере можно будет увидеть следующее:
dhcpd[7817]: DHCPDISCOVER from 08:00:27:99:a6:1f via enp1s0
dhcpd[7817]: DHCPOFFER on 192.168.0.150 to 08:00:27:99:a6:1f (host-199) via enp1s0
dhcpd[7817]: Commit: IP: 192.168.0.150 DHCID: 08:00:27:99:a6:1f Name: host-199
dhcpd[7817]: execute_statement argv[0] = /usr/local/bin/dhcp-dyndns.sh
dhcpd[7817]: execute_statement argv[1] = add
dhcpd[7817]: execute_statement argv[2] = 192.168.0.150
dhcpd[7817]: execute_statement argv[3] = 08:00:27:99:a6:1f
dhcpd[7817]: execute_statement argv[4] = host-199
dhcpd[8228]: 18-06-25 14:55:31 [dyndns] : Getting new ticket, old one has expired
dhcpd[8236]: 'A' record changed, updating record.
dhcpd[8237]: Record deleted successfully
dhcpd[8240]: Record added successfully
dhcpd[8268]: Record added successfully
dhcpd[8271]: DHCP-DNS add succeeded
dhcpd[7817]: DHCPREQUEST for 192.168.0.125 (192.168.0.132) from 08:00:27:99:a6:1f (host-199) via enp1s0
dhcpd[7817]: DHCPACK on 192.168.0.150 to 08:00:27:99:a6:1f (host-199) via enp1s0
Клиента можно найти как в прямой, так и в обратной зонах:
# host host-199
host-199.test.alt has address 192.168.0.150
host-199.test.alt has IPv6 address fd47:d11e:43c1:0:a00:27ff:fe99:a61f
# host 192.168.0.150
150.0.168.192.in-addr.arpa domain name pointer host-199.test.alt.