DDNS with unbound and kea
Last Update: 2023-11-14
My NixOS router uses unboud as a DNS server and kea as the DHCPv4/6 server (currently the IPv6 DHCP isn't working). This article contains a script for listening to changes in kea DHCP-leases and writing the hostnames to unbound. This gives your local devices domain names.
You might know that, kea also supports DDNS configuration. While that may be true, this only works, if the DNS server supports TSIG. The problem is that unbound doesn't support it. I looked into moving from unbound to a differnt DNS server (like Bind9 or Knot DNS), but those are complicated to configure with NixOS, so I stayed with unbound.
The solution: a bash script running as systemd service. The script itself requires that unbound can be remote controlled and the inotify-tools for listening to file changes.
#!/usr/bin/env bash
HOSTNAME="$(hostname).lan"
function readFile() {
if [[ "$2" == "A" ]] ; then
cat "$1" | tail -n +2 | while IFS=, read -r address hwaddr client_id valid_lifetime expire subnet_id fqdn_fwd fqdn_rev hostname state user_context
do
echo "${address},${hostname}"
done
else
cat "$1" | tail -n +2 | while IFS=, read -r address duid valid_lifetime expire subnet_id pref_lifetime lease_type iaid prefix_len fqdn_fwd fqdn_rev hostname hwaddr state user_context hwtype hwaddr_source
do
echo "${address},${hostname}"
done
fi
}
function readFileUnique() {
readFile "$1" $2 | uniq | while IFS=, read -r address hostname
do
if [[ "${hostname}" == *.$HOSTNAME ]] ; then
echo ${hostname} $2 ${address}
unbound-control local_data ${hostname} $2 ${address}
if [[ "$2" == "A" ]] ; then
echo ${address} | while IFS=. read -r ip0 ip1 ip2 ip3
do
unbound-control local_data ${ip3}.${ip2}.${ip1}.${ip0}.ip4.arpa. PTR ${hostname}
unbound-control local_data ${ip3}.${ip2}.${ip1}.${ip0}.in-addr.arpa. PTR ${hostname}
done
fi
fi
done
}
function syncFile() {
readFileUnique "$1" "$2"
while inotifywait -e close_write,create "$1" ; do
readFileUnique "$1" "$2"
done
}
syncFile "/var/lib/kea/dhcp4.leases" A &
syncFile "/var/lib/kea/dhcp6.leases" AAAA &
wait
This scripts reads the DHCP4/6-Leases CSV database stored by kea and then reads the address and hostname from that table. unbound-control is called to set the A
or AAAA
record. If it is IPv4, we reverse the IP-address and store a PTR record for reverse IP-lookup (dig -x YOUR.IP.GOES.HERE
). The wait at the end waits for the to forked syncFile
-functions to finish.
This script first reads all DHCP-leases and sets the DNS records accordingly and then waits for changes in the DHCP-leases with inotify.
Hostnames used for creating these DNS-Records must end here with your hostname + local-domain, That's why, you have to make sure that your kea DHCPv4/6 has the following configuration option:
ddns-qualifying-suffix = "YOUR-HOSTNAME.lan"
In your systemd-Service set PartOf
, After
and Wants
to unbound.service
. This will ensure that, the script is started, after unbound
and that the script is always restarted, when unbound itself is. WantedBy
must be set to multi-user.target
.
The configuration in NixOS is:
systemd.services.unbound-sync = {
enable = true;
path = with pkgs; [ unbound inotify-tools ];
script = ''
function readFile() {
if [[ "''\$2" == "A" ]] ; then
cat "''\$1" | tail -n +2 | while IFS=, read -r address hwaddr client_id valid_lifetime expire subnet_id fqdn_fwd fqdn_rev hostname state user_context
do
echo "''\${address},''\${hostname}"
done
else
cat "''\$1" | tail -n +2 | while IFS=, read -r address duid valid_lifetime expire subnet_id pref_lifetime lease_type iaid prefix_len fqdn_fwd fqdn_rev hostname hwaddr state user_context hwtype hwaddr_source
do
echo "''\${address},''\${hostname}"
done
fi
}
function readFileUnique() {
readFile "''\$1" ''\$2 | uniq | while IFS=, read -r address hostname
do
if [[ "''\${hostname}" == *.${config.networking.hostName}.lan ]] ; then
echo ''\${hostname} ''\$2 ''\${address}
unbound-control local_data ''\${hostname} ''\$2 ''\${address}
if [[ "''\$2" == "A" ]] ; then
echo ''\${address} | while IFS=. read -r ip0 ip1 ip2 ip3
do
unbound-control local_data ''\${ip3}.''\${ip2}.''\${ip1}.''\${ip0}.ip4.arpa. PTR ''\${hostname}
unbound-control local_data ''\${ip3}.''\${ip2}.''\${ip1}.''\${ip0}.in-addr.arpa. PTR ''\${hostname}
done
fi
fi
done
}
function syncFile() {
readFileUnique "''\$1" "''\$2"
while inotifywait -e close_write,create "''\$1" ; do
readFileUnique "''\$1" "''\$2"
done
}
syncFile "/var/lib/kea/dhcp4.leases" A &
syncFile "/var/lib/kea/dhcp6.leases" AAAA &
wait
'';
wants = [ "network-online.target" "unbound.service" ];
after = [ "network-online.target" "unbound.service" ];
partOf = [ "unbound.service" ];
wantedBy = [ "multi-user.target" ];
};