Ondrej Famera - top logo

DynDNS with NetworkManager and knot DNS (CentOS 7/8)

Having two servers from which only one has long term stable IP address which make it inconvenient when trying to reach it every time it changes IP. To deal with this I have used before a free DynDNS service hosted on Internet, but to remove dependency on it I wanted to try configure this completely “in-house”. I’m running my DNS zone on knot DNS server on CentOS 7 and I have a server with non-stable IP address running CentOS 8.

0. Overall architecture

The solution I’m running consists of knot DNS server that allows client (my other server) to update single DNS record when provided with correct TSIG key. The client running NetworkManager executes script each time the DHCP configuration has changed and updates the IP address when it thinks that it has changed according to local knowledge (file containing IP from previous run in temporary local file system).

1. Installing and configuring basic knot DNS server on CentOS 7

Knot DNS packages are available from EPEL repository on CentOS so the installation is as simple as enabling epel repository and installing the knot package.

# yum install epel-release
# yum install knot

After installation lets create a testing zone example.com in file /var/lib/knot/example.com.zone with content below. Note that we create a minimal zone with definition of one name server (dns.example.com) and 2 records that holds public SSH keys of client. These SSHFP records are optional and are here to demonstrate that we can restrict the update of DNS record to particular DNS record type. In such case you can have client with SSH key that is always same and cannot be changed while allowing client to update its A DNS record.

# cat /var/lib/knot/example.com.zone
example.com.   86400   SOA    dns.exmaple.com. ondrej-aighe0je.famera.cz. 2019120700 3600 15 86400 3600
example.com.   86400   NS     dns.example.com.
dns            86400   A      192.168.2.51
client         86400   SSHFP  4 1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
client         86400   SSHFP  4 2 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB

Update knot DNS /etc/knot/knot.conf configuration file. There are two changes when comparing content of the file to one installed by default:

  • First in server section we set the knot DNS to listen on all IPs (0.0.0.0:53) instead of just localhost(127.0.0.1:53).
  • Second we add definition for zone example.com that we will later need to allow particular clients to update it.
# cat /etc/knot/knot.conf
server:
    rundir: "/run/knot"
    user: knot:knot
    listen: [ 0.0.0.0@53, ::@53 ]

log:
  - target: syslog
    any: info

database:
    storage: "/var/lib/knot"

remote:
acl:

template:
  - id: default
    storage: "/var/lib/knot"
    file: "%s.zone"

zone:
  - domain: example.com
    file: "example.com.zone"

Enable dns service on firewalld, enable knot systemd service and start knot DNS server.

# firewall-cmd --add-service=dns --permanent
success
# firewall-cmd --add-service=dns
success
# systemctl enable knot
Created symlink from /etc/systemd/system/multi-user.target.wants/knot.service to /usr/lib/systemd/system/knot.service.
# systemctl start knot
# systemctl status knot
● knot.service - Knot DNS server
   Loaded: loaded (/usr/lib/systemd/system/knot.service; enabled; vendor preset: disabled)
   Active: active (running) since Sat 2019-12-07 16:47:00 KST; 1min ago
     Docs: man:knotd(8)
           man:knot.conf(5)
           man:knotc(8)
 Main PID: XXXX (knotd)
   CGroup: /system.slice/knot.service
           └─XXXX /usr/sbin/knotd

Dec 07 16:47:00 fastvm-centos-7-7 knotd[XXXX]: info: binding to interface 0.0.0.0@53
Dec 07 16:47:00 fastvm-centos-7-7 knotd[XXXX]: info: binding to interface ::@53
Dec 07 16:47:00 fastvm-centos-7-7 knotd[XXXX]: info: loading 1 zones
Dec 07 16:47:00 fastvm-centos-7-7 knotd[XXXX]: info: [example.com.] zone will be loaded
Dec 07 16:47:00 fastvm-centos-7-7 knotd[XXXX]: info: starting server
Dec 07 16:47:00 fastvm-centos-7-7 knotd[XXXX]: info: [example.com.] zone file parsed, serial 201...700
Dec 07 16:47:00 fastvm-centos-7-7 knotd[XXXX]: info: [example.com.] loaded, serial none -> 20191...tes
Dec 07 16:47:00 fastvm-centos-7-7 knotd[XXXX]: info: server started in the foreground, PID 9271
Dec 07 16:47:00 fastvm-centos-7-7 knotd[XXXX]: info: control, binding to '/run/knot/knot.sock'
Dec 07 16:47:00 fastvm-centos-7-7 systemd[1]: Started Knot DNS server.
Hint: Some lines were ellipsized, use -l to show in full.

If the server is running and status of it look similar to above, we can test from other machine if we can resolve the records from example.com domain. From some other machine test commands below. Command host is available from bind-utils package on CentOS 7/8, you can use any other command that can query DNS from specific host such as dig or kdig instead.

# host -t A dns.example.com. 192.168.2.51
Using domain server:
Name: 192.168.2.51
Address: 192.168.2.51#53
Aliases: 

dns.example.com has address 192.168.2.51
# host -t NS example.com. 192.168.2.51
Using domain server:
Name: 192.168.2.51
Address: 192.168.2.51#53
Aliases: 

example.com name server dns.example.com.

2. Configuring knot DNS server to allow updates authenticated via TSIG

Now as we have zone containing records we can configure server to accept changes from client that provides us with right TSIG key. Lets start by generating the key. For key generation we will use the keymgr utility that is part of knot package that is already installed on our sample DNS server. To generate a key using the hmac-sha512 algorithm we can use command below.

# keymgr -t client_abc_key hmac-sha512
...
# hmac-sha512:client_abc_key:aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffffgggggggggghhhhhhhhhhiiiiiiii
key:
  - id: client_abc_key
    algorithm: hmac-sha512
    secret: aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffffgggggggggghhhhhhhhhhiiiiiiii
...

Above command generated for us two very useful outputs.

  • First line is string that we can use on client to authenticate with nsupdate utility,
  • Another lines contain yaml block that we can use directly in knot DNS configuration file (/etc/knot/knot.conf).

Lets configure the server to accept the change of A record type for hostname client.example.com on our sample DNS server. We will add following things:

  • key block generated by key-generating utility to register TSIG key with DNS server
  • acl describing which action can be done with key and to which records (update)
  • and finally the example.com zone acl will list this newly created ACL so server knows that it applies to this zone Resulting /etc/knot/knot.conf file will look as shown below.
# cat /etc/knot/knot.conf
server:
    rundir: "/run/knot"
    user: knot:knot
    listen: [ 0.0.0.0@53, ::@53 ]

log:
  - target: syslog
    any: info

database:
    storage: "/var/lib/knot"

remote:
key:
  - id: client_abc_key
    algorithm: hmac-sha512
    secret: aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffffgggggggggghhhhhhhhhhiiiiiiii
acl:
  - id: client_abc_acl
    key: client_abc_key
    action: [ update ]
    update-type: [ A ]
    update-owner: name
    update-owner-name: [ client.example.com ]
    update-owner-match: equal

template:
  - id: default
    storage: "/var/lib/knot"
    file: "%s.zone"

zone:
  - domain: example.com
    file: "example.com.zone"
    acl: [ client_abc_acl ]

After changing the configuration lets reload the knot systemd service.

# systemctl reload knot
# systemctl status knot
...
Dec 07 16:49:18 fastvm-centos-7-7 systemd[1]: Reloading Knot DNS server.
Dec 07 16:49:19 fastvm-centos-7-7 knotd[XXXX]: info: control, received command 'reload'
Dec 07 16:49:19 fastvm-centos-7-7 knotd[XXXX]: info: reloading configuration file '/etc/knot/knot.conf'
Dec 07 16:49:19 fastvm-centos-7-7 knotd[XXXX]: info: configuration reloaded
Dec 07 16:49:19 fastvm-centos-7-7 knotc[YYYY]: Reloaded
Dec 07 16:49:19 fastvm-centos-7-7 systemd[1]: Reloaded Knot DNS server.
...

We can also check that currently running configuration contains references to ‘client_abc’ with command below.

# knotc conf-read|grep client_abc
key.id = client_abc_key.
key[client_abc_key.].algorithm = hmac-sha512
key[client_abc_key.].secret = aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffffgggggggggghhhhhhhhhhiiiiiiii
acl.id = client_abc_acl
acl[client_abc_acl].key = client_abc_key.
acl[client_abc_acl].action = update
acl[client_abc_acl].update-owner = name
acl[client_abc_acl].update-owner-match = equal
acl[client_abc_acl].update-owner-name = client.example.com.
acl[client_abc_acl].update-type = A
zone[example.com.].acl = client_abc_acl

3. Configuring client (NetworkManager) to update DNS record on IP change

For the client we would need following information:

  • TSIG string = first line generated by keymgr utility
  • DNS server IP address or hostname - in example here it will be IP 192.168.2.51
  • DNS record that we will be changing - in example here it will be client.example.com
  • name of the network interface on which the IP is assigned

Client will also need nsupdate utility (part of bind-utils package) to send update to DNS.

# yum install bind-utils

To get NetworkManager update the DNS record for us we will create following script in /etc/NetworkManager/dispatcher.d/12-ddns.sh. Important:: Adjust the variables in the “configurable variables” block of the script to match your settings. You can create script by hand (copy&paste from this page) or download it from git repository.

# curl https://raw.githubusercontent.com/OndrejHome/tp/master/2019-12-ddns-knot-dns-network-manager/12-ddns.sh > /etc/NetworkManager/dispatcher.d/12-ddns.sh
# cat /etc/NetworkManager/dispatcher.d/12-ddns.sh
#!/bin/sh
## == configurable variables == BEGIN
MONITORED_DEVICE="eth0"
TSIG_STRING="hmac-sha512:client_abc_key:aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffffgggggggggghhhhhhhhhhiiiiiiii"
DNS_SERVER="192.168.2.51"
DNS_HOSTNAME="client.example.com"
## == configurable variables == END

if [ "$1" != "$MONITORED_DEVICE" ] || [ "$2" != "dhcp4-change" ] && [ "$2" != "up" ]
then
    # if we have not matched the MONITORED_DEVICE or 
    # if the action was not the change of IP address, then lets quietly exit
    exit 0;
fi
# we have matched the event that IP was changed (or acquired) from DHCP
# first lets make sure we have tool to send updated IP to DNS
if ! command -v nsupdate >/dev/null; then
        logger "ddns: Missing 'nsupdate' command: NOT updating DNS."
	exit 0;
fi

# get IP address that we have set previously (this will be empty if run first time)
IP_OLD=$(cat /run/NetworkManager/current_ipv4 2>/dev/null)
# extract IPv4 address
IP=$(echo "$IP4_ADDRESS_0"|cut -d/ -f1)

# check if IPv4 address changed from previous time, continue only if there was change
# (or when this is the first time running = when IP_OLD is empty
if [ "$IP_OLD" = "$IP" ]; then
    logger "ddns: No change in IP address, not updating DNS."
    exit 0
fi

# update IPv4 into DNS
logger "ddns: Attempting to update IP to '$IP'."
nsupdate -y "$TSIG_STRING" <<EOF
server $DNS_SERVER
update delete $DNS_HOSTNAME a
update add $DNS_HOSTNAME 300 a $IP 
send
EOF

# store updated IPv4 address in temporary localfile so we skip update on DHCP refresh for same IP
echo "$IP" > /run/NetworkManager/current_ipv4

Change the permission on script file to make it both executable and protected from non-root users as the file contains the TSIG key that can change the DNS record.

# chmod 0700 /etc/NetworkManager/dispatcher.d/12-ddns.sh

4. Testing the setup

Easiest way to test if everything is working is to restart network on client (or simply reboot client) and check the logs. If the update was successful the logs will look similar to following.

On DNS server

# journalctl -f -u knot
...
Dec 07 18:20:48 fastvm-centos-7-7 knotd[XXXX]: info: [example.com.] DDNS, processing 1 updates
Dec 07 18:20:48 fastvm-centos-7-7 knotd[XXXX]: info: [example.com.] DDNS, finished, serial 2019120700 -> 2019120701, 0.04 seconds
Dec 07 18:20:48 fastvm-centos-7-7 knotd[XXXX]: info: [example.com.] zone file updated, serial 2019120700 -> 2019120701
...

On server you would see that new zone file was generated by knot-DNS server and that it contains the IP of client.

# cat /var/lib/knot/example.com.zone
;; Zone dump (Knot DNS 2.9.1)
example.com.        	86400	SOA	dns.exmaple.com. ondrej-aighe0je.famera.cz. 2019120701 3600 15 86400 3600
example.com.        	86400	NS	dns.example.com.
client.example.com.     86400   SSHFP   4 1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
client.example.com.     86400   SSHFP   4 2 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
client.example.com. 	300	A	192.168.2.52
dns.example.com.    	86400	A	192.168.2.51
;; Written 6 records
;; Time 2019-12-07 18:20:48 KST

On client

# journalctl -f
...
Dec 07 18:20:49 fastvm-centos-8-0 nm-dispatcher[XXXX]: req:4 'connectivity-change': new request (2 scripts)
Dec 07 18:20:49 fastvm-centos-8-0 root[YYYY]: ddns: Attempting to update IP to '192.168.2.52'.
Dec 07 18:20:49 fastvm-centos-8-0 nm-dispatcher[XXXX]: req:4 'connectivity-change': start running ordered scripts...
...

On client you should also be able to see file created by script containing current IPv4 address.

# cat /run/NetworkManager/current_ipv4 
192.168.2.52

Over the time you may also observe following message on the client if the IP has not changed since last time script run on client.

# journalctl -f
Dec 07 18:24:53 fastvm-centos-8-0 nm-dispatcher[XXXX]: req:4 'connectivity-change': new request (2 scripts)
Dec 07 18:24:53 fastvm-centos-8-0 root[YYYY]: ddns: No change in IP address, not updating DNS.
Dec 07 18:24:53 fastvm-centos-8-0 nm-dispatcher[XXXX]: req:4 'connectivity-change': start running ordered scripts...

5. Conclusion

With above setup you can use the self-hosted DynDNS with knot DNS as server allowing client to update only records that it is allowed to by specifying the TSIG key assigned to client. Client will update the IP address on every boot of system (because the file /run/NetworkManager/current_ipv4 in temporary /run file system would not exist) and also each time the new IP is received from DHCP. Server will automatically bump the zone ‘serial number’ and reload the zone file to reflect new change.

If you find the script interesting or you have ideas for improvement, feel free to check out OndrejHome/tp (tiny projects) repository or/and open Issue or Pull request agains it.

Version of packages used in examples

# rpm -qa|grep -E '(knot|bind-utils)'
knot-libs-2.9.1-1.el7.x86_64
knot-2.9.1-1.el7.x86_64
bind-utils-9.11.4-9.P2.el7.x86_64
# rpm -qa |grep -E '(NetworkManager|bind-utils)'
bind-utils-9.11.4-17.P2.el8_0.1.x86_64
NetworkManager-team-1.14.0-14.el8.x86_64
NetworkManager-tui-1.14.0-14.el8.x86_64
NetworkManager-1.14.0-14.el8.x86_64
NetworkManager-libnm-1.14.0-14.el8.x86_64
Last change .