Running a service which adds new hostnames to the internet requires a Dynamic DNS service. The uplift-bind package provides the client side of service interface for a Dynamic DNS based on ISC BIND DNS (or any other DNS service which honors RFC 2136 and 3007 dynamic update queries).

If you’re running a self-contained service for development or testing, you’ll need to have a local DNS service capable of accepting updates and responding to queries for your test zones while passing all other requests through to your external DNS servers.

For some simple unit testing you may instead want to have a completely isolated DNS service which can be initialized, updated, destroyed and reset without interfering with system operation.

This document describes how to configure DNS in both ways for different modes of testing. The first section describes and integrated service and the second describes an isolated one.

The initial steps assume that the host has a static IP address. Additional steps to configure a system with an IP address provided by DHCP follow.

1. Integrated Local DNS Service

This procedure will add a local named service which will accept updates using RFC compliant queries. A tool in bind-utils named nsupdate can be used to send updates for testing. The uplift-bind Ruby module uses rubygem-dnsruby to do the same thing.

For this procedure, we’re going to use a target domain of example.com. This domain is set aside by RFC 2606 for just this purpose. When it is done you will be able to query the example.com domain and get local answers. You will also be able to update the contents of the local example.com domain. Modify any instance of example.com in the procedure below if you wish to use another domain for your configuration.

Nearly all of the commands below must be run as root or using sudo(8).

All other queries will be forwarded to the normal external DNS service.

1.1. Install BIND

You should just be able to request the bind package. The bind-utils package is needed for verification and testing.

$ yum install bind bind-utils

1.2. Generate DNS Update Keys

There are several good sites that go into details about how to create DNSSEC keys. See the references section. For now I’m just going to give one example.

Note that dnssec-keygen needs a source of "entropy". If it appears to hang, log onto the host with another session and type or execute a few commands until enough entropy has been generated to complete the key generation.

$ dnssec-keygen -a HMAC-MD5 -b 512 -n USER example.com.

If this step hangs, you probably don’t have enough entropy on your system.

You can solve this in one of two ways:

  • use -r /dev/urandom:

    $ dnssec-keygen -a HMAC-MD5 -b 512 -n USER -r /dev/urandom example.com.
  • generate more entropy (randomness) on your system

The first method will give you potentially weaker keys. The second requires you to type a lot or generate a lot of network traffic until the entropy pool (in /dev/random) is large enough to provide input for your key.

The key generation process (dnssec-keygen) will produce two files who’s names look something like this:

 Kexample.com.+157+30572.key
 Kexample.com.+157+30572.private

The key string is in both files but you can extract it most easily like this:

$ grep Key: Kexample.com.*.private | cut -d' ' -f 2

(assuming you only have one private key file in your current working directory)

Copy the files to /var/named for safe keeping. We’ll need the key string later for configuring /etc/named.conf

1.3. Create Initial Test Zone Files

DNS updates have to be sent to a specific zone. You need to have the example.com zone configured into your local named as a dynamic zone. There is a sample initial example.com.db file included in the examples directory.

One thing to note is that the example has the default TTL value set to 1 second. This is to avoid testing errors caused by caching. If the TTL is larger then changes will not be reflected until the record times out. As it is, delete test queries should wait 2 seconds after the delete operation completes to be sure to get a correct answer. You would never use this value in production.

1.4. Configure Named

The master configuration file for ISC BIND named is /etc/named.conf. There are several significant settings in that file which bear pointing out.

1.4.1. Forwarding

The local server must be set to forward only. While recursion is enabled, it will not be used. Requests for zones which are not locally authoritative (basically everything but example.com) will be forwarded to the upstream DNS server.

The forwarders clause in the configuration will be included from a file named /var/named/forwarders.conf. This will allow you to update the forwarders without editing the named.conf itself. This will become important if you’re getting your primary IP address from DHCP.

The forwarders section consists of the forwarders keyword and a block of semi-colon terminated IP addresses. These addresses should be the addresses you would normally have in your /etc/resolv.conf nameserver list.

forwarders { <ip address 1> [ ; <ip address N ]... ; } ;

For now you can create /var/named/forwarders.conf by hand.

1.4.2. Update Keys

Each dynamic zone requires an associated update key. The nameserver and client each have a copy of the same key. The keys have an id and a value. The id is the string provided at the end of the dnssec-keygen(8) command above and the value is the string we extracted from the K*.private file.

The sample named.conf file includes the key configuration from a file called example.com.key.

include "example.com.key";

The key file contains the key definition section which looks like this:

key example.com {
  algorithm HMAC-MD5 ;
  secret "<key string>" ;
} ;

Substitute the key string from the private key file generated earlier and place the key file in /var/named/example.com.key.

1.4.3. Test Zones

The test zones are set in the /etc/named.conf file with a zone section. The zone file itself was described above. Here we specify the type of zone, the zone file location and the fact that it can be updated using the key included in the previous section.

The sample named.conf has a zone section for the example.com zone:

 zone "example.com" IN {
 	type master;
 	file "dynamic/example.com.db";
 	allow-update { key example.com ; } ;
 };

1.5. Start named Service

First, you want to test that the configuration files are valid and free of typos. You can start a named manually and observe the startup using the -g option:

$ /usr/sbin/named -g

If there are any errors, check the log output and the contents of /var/log/messages for syntax and configuration errors.

When you’re satisfied that the configuration is correct, interrupt the named with CTRL-C and start it as a proper service:

$ service named start

1.6. Test Local Queries

Once the named is running you can check that it is responding to queries. The tools for that are in the bind-utils RPM. Install that if you haven’t yet. The two tools for testing ordinary queries are dig(1) and host(1). Dig gives more detailed output and does not use the domain or search lines from /etc/resolv.conf. You have to provide fully qualified domain names for queries. Host gives more compact simple output and does use resolv.conf to complete partial names.

Because the resolv.conf does not yet have 127.0.0.1 as the first nameserver you have to specify the nameserver on the query command line.

$ dig @127.0.0.1 example.com soa

or

$ host -t soa example.com 127.0.0.1

Those will show the Start Of Authority records for the example.com zone. They should reflect the values in your local zone. Compare them to the values you get from the same query on a host using normal DNS.

1.7. Test Remote Queries

With forwarders configured, you should also be able to get responses for zones outside your test zone.

$ dig @127.0.0.1 icann.org a

or

$ host -t a icann.org 127.0.0.1

These should complete promptly and show the normal IP address values.

1.8. Test Updates

Update testing uses another tool from bind-utils named nsupdate(1). nsupdate takes its input from standard input. It also requires the key for authentication. You can test adding a record like this: (note the extra spaces in the indentation of the example)

 nsupdate -k /var/named/example.com.key <<EOF
 server 127.0.0.1
 update add testaddr.example.com 1 A 192.168.254.254
 send
 quit
 EOF

Following successful completion of that command you should be able to query with dig(1) or host(1) and verify that the new record is there.

$ dig @127.0.0.1 testhost.example.com a

1.9. Enable named Service

When you’re satisfied that the service is running and responding correctly you can enable the system service so that it restarts on reboot:

$ chkconfig named on

1.10. Set Resolver Order

The final step to integrating the local named is to make it the first nameserver in your resolver list. Once this is done all queries will go to the local named first by default.

Add the following line to your /etc/resolv.conf file before any other nameserver lines:

nameserver 127.0.0.1

2. DHCP Updates

If your network interface is set statically, you can stop here. Otherwise, read on.

For a self-contained service which would be typical for testing, or for a virtual host environment like EC2 it is possible that the DNS host will get its IP address and DNS information from DHCP. In that case each time the host renews its DHCP lease it will overwrite the /etc/resolv.conf file. It may also change its upstream nameserver list. If that happens, the forwarders list for the named must also change.

dhclient is the daemon that maintains DHCP controlled interfaces. It has hooks which can be used to run scripts triggered on lease renewals. However in RHEL and Fedora distributions the current default manager for interfaces is NetworkManager. NetworkManager is designed mostly for mobile device users and does not seem to provide access to the kinds of control hooks that dhclient does.

To provide the control needed we’re doing to disable NetworkManager and let interface control fall back to the more primitive network service and the dhclient daemon.

Note that the forwarders update below will fail if you have SELinux enabled. If you’re running with SELinux disabled skip down past the SELinux instructions.

If you consider it safe you can temporarily suspend SELinux and re-enable it later.

$ setenforce 0

2.1. SELinux

If you don’t have SELinux enabled, you can skip to the next section. But you should really, really be using SELinux!

If you are running your host with SELinux enabled then the dhclient service will not have permission to write any file which the named service has permission to read. You will need to extend the SELinux policy to allow the dhclient-up-hooks script to write the /var/named/forwarders.conf file and make it readable.

To compile and load the new policy you will need the selinux-policy and policycoreutils RPMs installed. If you have SELinux enabled you will certainly already have the selinux-policy package. You may still need to install policycoreutils.

$ yum install selinux-policy policycoreutils

The examples directory contains two files which define a policy update that does just that:

dhcpnamedforward.te
dhcpnamedforward.fc

The first is a set of new policy rules. The second defines the default label for the /var/named/forwarders.conf file so that the rules will apply.

2.1.1. Compiling the Policy Module

Copy the policy files to /usr/share/selinux/packages.

Compile the policy module:

$ cd /usr/share/selinux/packages
$ make -f /usr/share/selinux/devel/Makefile

This will generate two additional files.

dhcpnamedforward.if (1)
dhcpnamedforward.pp (2)
1 The .if file is an empty "interface" template and can be ignored.
2 The .pp file is the compiled policy. This is what gets loaded.

2.1.2. Installing the Policy Module

To load the policy module use semodule(8)

$ semodule -i /usr/share/selinux/packages/dhcpnamedforward.pp

This could take a couple of minutes. When it completes, check that the module is installed:

$ semodule -l | grep dhcpnamedforward

At this point SELinux should allow the dhclient-up-hooks script to write /var/named/forwarders.conf and the named service to read it.

2.2. Disable NetworkManager Service

Attempt to disable NetworkManager service:

$ chkconfig NetworkManager off

If you get any errors it’s likely that you don’t have NetworkManager installed and life is good.

Then enable the generic "network" service

$ chkconfig network on

And change any interfaces that think they’re controlled by NetworkManager and change them over:

$ grep -l NM_CONTROLLED /etc/sysconfig/network-scripts/ifcfg-* | xargs perl -p -i -e '/NM_CONTROLLED/ && s/yes/no/i'

2.3. dhclient Config (Resolver Prefix)

As noted earlier, dhclient will rewrite the /etc/resolv.conf file each time it renews the DHCP lease. You can configure it to put a value before the other nameserver lines. Create a file named /etc/dhcp/dhclient.conf and put this in it:

# prepend localhost for DNS lookup in dev and test
prepend domain-name-servers 127.0.0.1 ;

2.4. dhclient Hooks (Update Forwarders)

dhclient also has the capability to run a script when an interface comes up. If you place a Bourne shell script at /etc/dhcp/dhclient-up-hooks and make sure it’s readable and executable then it will be sourced when any interface renews its lease.

The dhclient-up-hooks script in the directory which contains this README will create a file named /var/named/forwarders.conf on lease renew.

2.5. named.conf: Include Forwarders

If you followed the instructions initially your /etc/named.conf file already includes the /var/named/forwarders.conf to set the forwarders list. If not, do it now.

2.6. Renewing the Interface

Now if you force the external interface to renew (do this while logged in via serial console!) you should be able to watch the forwarders be updated and the named reloaded to get the update

$ service network restart

If you get an error or you don’t see the timestamp change on /var/named/forwarders.conf then check the execute bit on /etc/dhcp/dhclient-up-hooks.

3. Isolated Local DNS Service

This section describes how to create a small self-contained DNS service suitable for testing dynamic DNS operations.

The idea is to run a local DNS server as a non-root user on a non-standard port for testing purposes. This configuration will not forward requests and will not interact with regular system DNS lookups.

3.1. Install required software

The DNS service requires the bind RPM

$ yum install bind

3.2. Create a Workspace

Create a space to run the local service

$ mkdir ~/ddns

Create a space for temporary files and logs

# mkdir ~/ddns/tmp

3.3. Populate the Workspace

Copy the stock/default named configuration files

$ cd ~/ddns
$ sudo cp /etc/named.* .
$ sudo cp /var/named/named.{ca,empty,localhost,loopback} .
$ sudo chown `id -u`:`id -g` *

Comment out IPv6 root servers (unless you have IPv6 configured):

$ sed  -i -e '/AAAA/ && s/^/;;/' named.*

3.4. Enable Secure Updates

Generate update keys: may need enough randomness. Log in and type stuff:

$ dnssec-keygen -a HMAC-MD5 -b 512 -n USER example.com

Extract the key value:

$ perl -n -e '/Key: / && s/Key: // && print' Kexample.com.*.private

example.com.key:

 key example.com {
   algorithm HMAC-MD5;
   secret "H6NDDnTbNpcBrUM5c4BJtohyK2uuZ5Oi6jxg3ME+RJsNl5Wl2B87oL12 YxWUR3Gp7FdZQojTKBSfs5ZjghYxGw==";
 };

3.5. Create the Test Configuration File

This file is a limited configuration. It runs on a non-standard high-numbered port. It runs from a single directory and stores run-time files in a temporary directory so they can be cleaned up and repopulated easily

named.conf
 // named.conf

 options {
 	listen-on port 10053 { 127.0.0.1; };
 	directory 	".";
 	allow-query     { localhost; };
        recursion no;

 	pid-file "tmp/named.pid";
 	session-keyfile "tmp/named.session.key";
        managed-keys-directory "tmp";
 };

 // disable remote controls
 controls {};

 logging {
         channel default_debug {
                 //file "data/named.run";
                 file "tmp/named.log";
                 severity dynamic;
         };
 };

 include "named.rfc1912.zones";

 //
 // Local customization
 //

 include "example.com.key";

 zone "example.com" IN {
      type master;
      file "tmp/example.com.db";
      allow-update { key example.com ; };
 };

3.6. Zone Files

Create the template zone files in the main directory. You will copy them to the tmp directory for test runs. Changes to the running service will cause changes to the zone files.

example.com.db
 ; initial data for testing DDNS using BIND
 $ORIGIN .
 $TTL 1	; 1 seconds (for testing only)
 example.com		IN SOA	ns1.example.com. hostmaster.example.com. (
 				2011112904 ; serial
 				60         ; refresh (1 minute)
 				15         ; retry (15 seconds)
 				1800       ; expire (30 minutes)
 				10         ; minimum (10 seconds)
 				)
 			NS	ns1.example.com.
 			MX	10 mail.example.com.
 $ORIGIN example.com.
 mail			A	127.0.0.1
 master			A	192.168.1.1
 ns1			A	127.0.0.1
 node                    A       192.168.1.10

 ; test records
 testns1			TXT	"reserved namespace testns1"
 ;testns2		TXT	"to be added by tests"
 testns3                 TXT     "reserved to add apps"
 testns4                 TXT     "reserved to delete apps"
 testapp4-testns4        CNAME   node.example.com.

3.7. Testing

Go to the ddns working directory:

$ cd ~/ddns

Clear the tmp directory

$ rm -f tmp/*

Copy the initial zone files

$ for FILE in  *.init ; do cp $FILE tmp/`basename $FILE .init` ; done

Start the named; log to stdout, no fork:

$ /usr/sbin/named -c named.conf -g

Add an A record and try to retrieve it:

 nsupdate -y HMAC-MD5:example.com:`perl -n -e '/secret "([^"]+)"/ && print $1;' example.com.key` <<EOF
 server localhost 10053
 update add foo.example.com 1 A 192.168.1.2
 send
 EOF

Check the logs for the entry record:

$ grep foo tmp/named.log

Check that the named returns the new record

$ dig -p 10053 @localhost foo.example.com

Stop the named instance:

$ kill `cat tmp/named.pid`

4. References