« Back to home

Running BIND9 and ISC-DHCP

Running BIND9 and ISC-DHCP

Most people use a NAT router at home for connecting to the Internet, and most consumer-grade NAT routers offer some limited version of DHCP for automatically handing out IP addresses to desktops and laptops and game consoles and smartphones and some limited version of DNS for making sure all the devices on the network know what all the other devices are called. However, the feature set and functionality of these cut-down DHCP and DNS instances are almost always too limited to handle more than the simplest of network designs; sometimes, you need to be able to do more. For example, if you wanted to set up a separate DHCP zone for handing out addresses to untrusted wireless clients versus trusted clients, or if you wanted to do something more awesome like implement the Upside-Down-Ternet, you'd need something a lot more configurable than the little NAT router's applications.

There are lots of options, but it's easiest to just pull out the big guns and set up BIND9, the current version of the DNS software that powers the Internet, along with the ISC's DHCP server. DNS and DHCP are like peas and carrots, as the saying goes—DHCP hands out the addresses, but doesn't communicate to other network hosts who has what address; DNS knows how to correlate names to addresses but doesn't hand out addresses itself. In this post, we'll set up DNS and DHCP on Ubuntu, and then configure them to work together.

(NB. This blog entry ended up being bloody huge, because I don't just list the configuration options to set but rather go into detail on what each one does. I'd intended to bang the post out in a single evening, but instead it's taken a couple of hours over three days to complete. I hope it is informative and helpful!)

This tutorial has been done more than once, to be sure. You can search around and find tons of other writeups about deploying DNS and DHCP and getting them to update each other. However, no single tutorial worked for me—so, this is a write-up of everything I had to do to get the end-to-end solution working.


First, the necessary installations. Both BIND9 and DHCP are are available in handy-dandy prepackaged form if you're running Ubuntu or another Debian-based distro:

$ sudo aptitude install bind9 isc-dhcp-server

In versions of Ubuntu prior to 11.04, the DHCP server package was simply called dhcp3-server; the 11.04 and later repositories have the updated isc-dhcp-server package in its place (which is version 4, though why they didn't just call it dhcp4-server is beyond me).

If you're running DHCP and DNS on your NAT router, now would be a good time to disable it. How to do this varies depending on the type of router, but if you're willing to set up your own DHCP and DNS services on a Linux server, you can probably figure it out.

DNS configuration

BIND9 is a Unixy-beast—it's a powerful but cryptic application with a rich and potentially complex configuration. Still, all we're going to do is set up three simple zones—that is, three separate administrative blocks—and add a few options so that servers on your LAN can use the DNS server, so the resultant set of config files won't be too bad at all. The configuration for the DHCP server will be a lot more complex, so we'll save it for last.

When installed from a package, the configuration files for BIND9 are located in /etc/bind. The main configuration file is named.conf (that's "name-d", as in the daemon controlling the naming service, not "named" as in the past tense verb), which really just functions as a container and references the other three configuration files:

// Default contents of /etc/bind/named.conf
// This is the primary configuration file for the BIND DNS server named.
// Please read /usr/share/doc/bind9/README.Debian.gz for information on the 
// structure of BIND configuration files in Debian, *BEFORE* you customize 
// this configuration file.
// If you are just adding zones, please do that in /etc/bind/named.conf.local

include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.local";
include "/etc/bind/named.conf.default-zones";

As the file says in the comments, this isn't the place to do any actual work. We need to dive into named.conf.local and named.conf.options.

First, the options. I've modified my named.conf.options file so that it looks like this:

options {
    directory "/var/cache/bind";
    auth-nxdomain no;    # conform to RFC1035
    forwarders {;;
    allow-query {
    allow-transfer {

The forwarders section contains servers that the DNS server will check if it doesn't have a record of the host you're trying to reach. Since the DNS server won't have a record of anything outside of your LAN, you'll need to put your ISP's or some other entity's DNS server IP addresses here. The two I have listed belong to Google. Your DNS server will eventually build up a good cache of sites you visit regularly, but it still will often need to query external name servers.

The allow-query line contains the subnet(s) and netmask(s) that are allowed to send DNS queries to the server. You obviously want to constrain this to your local LAN.

The allow-transfer segment lists which subnets and netmasks are allowed to get copies of the DNS server's zone data. Best to set this to the local LAN as well. It's not something you'll need to worry about unless you're running multiple DNS servers, though.

Next, we have to decide what zones we need. I'll be defining three DNS zones for my LAN—the first will be a forward lookup zone for my LAN's main network segment, which the DNS server will use to correlate host names to IP addresses. However, in order to do the opposite and correlate IP addresses to names, a separate zone must be created called a reverse lookup zone. Finally, I'll also create a special zone for *.facebook.com, which will prevent any hosts using this DNS server from resolving any Facebook IP addresses, because I don't use Facebook and I dislike the load delays its bugs and banners introduce into web pages.

Dynamic updating: DNS

Before we add those zones into the configuration file, though, we have to also do some cryptographic voodoo. This whole exercise of building zones is kind of pointless without dynamic updating; that is, without the ability of the DHCP server to update the DNS zones with the addresses it hands out and the host names those addresses are assigned to. So, we need to generate a cryprographic hash which the DNS and DHCP servers both have access to. To do this, run the following command:

/usr/sbin/rndc-confgen -a

This will create a file named rndc.key, whose contents will look something like this:

key "rndc-key" {
    algorithm hmac-md5;
    secret "HFQYu0RsxJ/DJb3dyZQNQ==";

Creating DNS zones

Copy this file to the clipboard, as we're now reqdy to configure our DNS zones, and the very first thing to put into the config file will be the contents of rndc.key. Open up named.conf.local for editing and make it look like this:

key "rndc-key" {
    algorithm hmac-md5;
    secret "HFQYu0RsxJ/DJb3dyZQNQ==";

zone "bigdinosaur.org" {
    type master;
    file "/var/lib/bind/bigdinosaur.org.hosts";
    allow-update { key rndc-key; };

zone "10.10.10.in-addr.arpa" {
    type master;
    file "/var/lib/bind/10.10.10.rev";
    allow-update { key rndc-key; };

zone "facebook.com" {
    type master;
    file "/var/lib/bind/dummy-block";

This defines the three zones we need. The first zone is my forward lookup zone for Bigdinosaur.org, and the entry tells the DNS server that the IP addresses for all host names ending in "bigdinosaur.org" can be found in the file /var/lib/bind/bigdinosaur.org.hosts. The second zone is the reverse lookup zone for the RFC 1918 netblock, and tells the DNS server that the host names for all IP addresses between and can be found in the file /var/lib/bind/10.10.10.rev. The third zone is for *.facebook.com, and tells the DNS server that any hostnames ending in "facebook.com" can be found in /var/lib/bind/dummy-block.

Dynamic updates of the forward and reverse zones is controlled by the allow-update line. This line says that a DHCP server which presents a hash matching the one defined by the key section is allowed to make modifications to that zone. Here, we're allowing the DHCP server (which we'll also configure with the same key) to update the Bigdinosaur.org forward lookup zone and also the reverse lookup zone. We don't need the server to be able to update the dummy Facebook zone, since we won't be assigning out any DHCP addresses to that zone.

Alternately, instead of cutting and pasting in the contents of rndc.key, you can simply add an include line for it, like this:

include "/etc/bind/rndc.key"

The generated key file is automatically set to be owned by and only readable by the bind user, so using an include statement instead of actually having the code block in named.conf.local is more secure. However, this is just a home DNS server, so it's not really necessary.

DNS zone population

Now that the zones have been created, we'll need to populate them with basic data, as well as the IP addresses and network names of any statically-addressed hosts on the network. Don't worry about filling in the names for any DHCP-assigned hosts, as the dynamic update setting we've just finished with will take care of allowing DHCP to add in its own hosts without you having to deal with it.

The first file we need to modify is the forward lookup zone definition, we defined a moment ago to be /var/lib/bind/bigdinosaur.org.hosts:

# This line indicates that the object we're configuring below (in this case,
# bigdinosaur.org) has its origin at the "." domain.  "." is the root domain
# from which all the TLDs branch.

# Next line defines the DNS time-to-live setting
$TTL 907200	; 1 week 3 days 12 hours

# The next set of lines are the "Start of Authority" record and define important
# info about the domain. In my case, we're defining bigdinosaur.org and saying
# that dnsserver.bigdinosaur.org is its source host, and webmaster@bigdinosaur.org
# is the domain maintainer. For the e-mail address, we use a dot instead of an @.
# The lines after that define the zone serial number, which is used to keep track
# of when the zone file was modified, and then some interval definitions which
# you can leave as default.
bigdinosaur.org		IN SOA	dnsserver.bigdinosaur.org. webmaster.bigdinosaur.org. (
                1263527838 ; serial
                10800      ; refresh (3 hours)
                3600       ; retry (1 hour)
                604800     ; expire (1 week)
                38400      ; minimum (10 hours 40 minutes)

# Next, we define the hosts necessary to make the domain function. First, we add
# an "NS Record" to define the domain's name server...
            NS	dnsserver.bigdinosaur.org.

# ...then an "A Record" for the domain server's IP address...

# ...and finally "MX Records" so that e-mail for the domain's e-mail addresses
# goes to the right place. Since my domain is registred through Google Apps,
# this info was all provided by Google.
            MX	10 aspmx.l.google.com.
            MX	20 alt1.aspmx.l.google.com.
            MX	20 alt2.aspmx.l.google.com.
            MX	30 aspmx2.googlemail.com.
            MX	30 aspmx3.googlemail.com.
            MX	30 aspmx4.googlemail.com.
            MX	30 aspmx5.googlemail.com.
# Now we're ready to begin adding hosts, but first we need another origin
# statement to indicate that the hosts added below originate not from ".", like
# the domain itself; rather, they originate from "bigdinosaur.org".
$ORIGIN bigdinosaur.org.
# Now we add A records for the non-DHCP hosts in the domain:
server_1		A
server_2		A
server_3		A

As indicated above, the first part of the file defines basic info about the zone, and the second part defines the hosts. Each statically-addressed host gets an A record so that the server knows how to correlate its name with its IP address.

Now that the forward zone is built, we need to build the reverse zone, so that we can correlate IP addresses back to names, just like a reverse phonebook. The reverse zone we defined earlier is located at /var/lib/bind/10.10.10.rev, and should look something like this:

# Again, we have an origin record and a TTL entry...
$TTL 907200	; 1 week 3 days 12 hours

# ...but note the name of the reverse domain: "10.10.10.in-addr-arpa". This is a 
# special name format used only by reverse lookup domains.
10.10.10.in-addr.arpa	IN SOA	dnsserver.bigdinosaur.org. webmaster.bigdinosaur.org. (
                1263187356 ; serial
                10800      ; refresh (3 hours)
                3600       ; retry (1 hour)
                604800     ; expire (1 week)
                38400      ; minimum (10 hours 40 minutes)
            NS	dnsserver.bigdinosaur.org.
# Just like above, we now set our origin away from "." to the actual domain name,
# which is "10.10.10.in-addr-arpa", and then we add records. However, this time,
# we're adding "PTR records", or pointer records.
$ORIGIN 10.10.10.in-addr.arpa.
21			PTR	server_1.bigdinosaur.org.
22			PTR	server_2.bigdinosaur.org.
23			PTR	server_3.bigdinosaur.org.

The thing to notice about the reverse zone is the name of the domain we're working with: 10.10.10.in-addr.arpa. The .arpa domain is a legacy domain from the early days of the Internet; in-addr.arpa is used as the domain for reverse lookups for historical reasons, because DNS reverse lookups use a method codified back when .arpa was actually a working domain.

The very last zone to define is our "dummy" zone, which we use to blackhole all Facebook.com DNS lookups. This file is defined at /var/lib/bind/dummy-block:

$TTL 24h

@IN SOA dnsserver.bigdinosaur.org. webmaster.bigdinosaur.org. (
    2003052800  86400  300  604800  3600 )

@	IN	NS	dnsserver.bigdinosaur.org.
@	IN	A
*	IN	A

This file's structure is much simpler and adds an A record to return an IP address of for all host name lookups it services.

To make all of our changes in all of the above files active, restart the DNS service with sudo /etc/init.d/bind9 restart.

Configuring DHCP

Our DHCP configuration touches only one file, but has a big chunk of options we need to set for dynamic updating to work; additionally, there are some decisions to be made about how and to which hosts addresses should be distributed.

We'll be modifying /etc/dhcp/dhcpd.conf, which contains all the configuration settings for the DHCP server. The file is organized into four sections: configuration directives, the DNS zones we're allowed to update, the DHCP scope definition and scope-specific configuration directives, and DHCP groups; we'll tackle them one at a time.

Don't be me: carefully double-check your syntax!

A cautionary note: pay extremely close attention to syntax, especially punctuation. Many of the options, particularly in the second, third, and fourth sections, are very similar to some of the lines from the DNS configuration, but "very similar" is not "exactly alike". Cut and paste with caution—when I was originally setting this all up, I ran into a problem which had me banging my head against the wall for almost a full hour, and which all ended up coming back to a single misplaced period. Don't be me: carefully double-check your syntax!

Global options

Here's the option block in my dhcpd.conf:

ddns-updates on;
ddns-update-style interim;
update-static-leases on;
key rndc-key { algorithm hmac-md5; secret HFQYu0RsxJ/DJb3dyZQNQ==;}
allow unknown-clients;
use-host-decl-names on;
default-lease-time 1814400; #21 days
max-lease-time 1814400; #21 days
log-facility local7;

And, line by line, here's what we're doing:

ddns-updates on;: This line enables global dynamic updating. You can also set this per-scope, in case you wanted some scopes to be able to do updating and not others, but since in this example we'll only configure one scope, we can make the option global.

ddns-update-style interim;: Sets the style of dynamic updating. It can be "interim", which is what we want, "off", which disables dynamic updating, or "ad-hoc", which refers to a deprecated dynamic update style and shouldn't be used.

update-static-leases on;: Tells the DHCP server to do DNS updates even for clients with "static leases"; that is, clients who receive a DHCP address that you specifically assign them based on MAC address. We're going to define a few static leases further down, and this option is useful in that it saves you from having to manually update DNS entries for those clients if you ever want to change their addresses.

authoritative;: Tells the DHCP server that it is to act as the one true DHCP server for the scopes it's configured to understand, by sending out DHCPNAK (DHCP-no-acknowledge) packets to misconfigured DHCP clients. You always want authoritative set if this is your only DHCP server; without it, the server won't tell misconfigured clients that they're misconfigured, and some clients might not get IP addresses immediately on requesting them.

key rndc-key: Here's where we put our crypto hash we generated during the DNS configuration section earlier. This sets "rndc-key" as a key variable so that when we define our DHCP scope (or scopes), we can reference just the name of the key instead of having to have the whole thing in each scope statement.

allow unknown-clients;: Tells the DHCP server to assign addresses to clients without static host declarations, which is almost certainly something you want to do. Otherwise, only clients you've manually given addresses to later in the file will get DHCP assignments.

use-host-decl-names on;: Tells the DHCP server to tell static-mapped clients what their hostname is via the "hostname" option inside the DHCP response. This is a legacy option that I've left on because in some cases it can simplify your DHCP server configuration; most clients ignore the "hostname" option entirely.

default-lease-time and max-lease-time: Set how log the DHCP leases are good for. This is the maximum time a client will go before it asks for a new address. This option can be set longer or shorter to taste; for a small home LAN, it doesn't make much difference what it's set to.

log-facility local7;: Sets debug logging. We want this at least at first because it will help identify any DHCP config problems. After things are verified good, we can scale this back to local2.

DNS zones

The next section of the file defines the DNS forward and reverse zones inside of which our DHCP server will be operating, as well as the key that we'll have to use to do dynamic updating in those zones. Here we'll be using the information we established earlier in the DNS configuration portion of this entry.

# Bigdino DNS zones
zone bigdinosaur.org. {
    primary localhost; # This server is the primary DNS server for the zone
    key rndc-key; # Use the key we defined earlier for dynamic updates
zone 10.10.10.in-addr.arpa. {
    primary localhost; # This server is the primary DNS server for the zone
    key rndc-key; # Use the key we defined earlier for dynamic updates

DHCP scope

Now we define our DHCP scope. A "scope" in DHCP terminology is a chunk of your network which will be assigned dynamic IP addresses and DHCP configuration options out of the same bucket. If you had two different blocks of network addresses—say, one for wireless clients and one for wired—you'd build two scopes. Here, we're only going to construct one, and set some scope-specific configuration options on it:

# Bigdino LAN scope
subnet netmask {
    option subnet-mask;
    option routers;
    option domain-name-servers;
    option domain-name "bigdinosaur.org";
    ddns-domainname "bigdinosaur.org.";
     ddns-rev-domainname "in-addr.arpa.";

The scope declaration begins with the address pool and subnet mask inside of which all DHCP-able addresses are located (the subnet line), and then defines a specific range of dynamic addresses it is allowed to hand out. Note that my range is included in, but doesn't take up all of, the declared subnet; the range setting defines the addresses that will be handed out to unknown clients, like your buddy's iPhone when he comes over. In the final section of the config file we're going to define some static hosts, and those hosts will get addresses inside the scope, but outside of the unknown client range.

The lines beginning with option control information sent out inside the initial DHCP responses from the server to clients—in order, I'm defining the subnet mask clients get, the network gateway ("router") they get, the DNS server they get, and the domain name suffix they are told to apply to their hostnames. Finally, the last two lines telll the DHCP server which DNS forward and reverse zone names to update when clients get addresses. One thing to note is that the ddns-rev-domainname is simply set to in-addr.arpa. instead of the full name of the reverese zone; this is okay, because the DHCP server is smart enough to figure out what the reverse zone's full name is based on the addresses it's handing out.

Static hosts

The last section is where we define our static DHCP hosts. This is different from hosts with true static IP addresses, because static IP addresses are set on the client site, whereas statically-assigned DHCP addresses are set here, in the server's configuration file.

Statically-assigned DHCP addresses are handy if you want some hosts to always have the same IP address, but don't want to have to sit down at each host and enter its address information manually. Plus, if the name of the DNS server or any of the domain info ever changes, you can update it at the DHCP server instead of having to track down each of the hosts. I statically assign all of my LAN hosts, and then leave the DHCP server to hand out addresses from its pool to other unknown devices (buddies' laptops and phones when they come over, mainly).

# Bigdinosaur.org group
group {
    # Host number one
    host firstlanhost.bigdinosaur.org {
        hardware ethernet 00:00:00:00:00:01;
        ddns-hostname "firstlanhost";
    # Host number two
    host secondlanhost.bigdinosaur.org {
        hardware ethernet 00:00:00:00:00:02;
        ddns-hostname "secondlanhost";
    # Host number three
    host thirdlanhost.bigdinosaur.org {
        hardware ethernet 00:00:00:00:00:03;
        ddns-hostname "thirdlanhost";

The host declarations are pretty easy to follow—you define the fully-qualified domain name of the host, followed by the MAC address (which is how the DHCP server identifies the host), the address you want the host to receive, and the name that the DHCP server will use in its dynamic update call back to the DNS server.

Bringing it all together

After a whole lot of configuration work, we're ready to try it out. Bounce both the DNS and DHCP services:

$ sudo /etc/init.d/bind9 restart
$ sudo /etc/init.d/isc-dhcp-server restart

Then, have one of your LAN boxes try to grab an IP address. Tail the syslog file to watch the process as as it moves along:

$ tail -f /var/log/syslog
Jan 21 12:14:35 dnsserver dhcpd: DHCPREQUEST for from 00:1c:42:0f:bc:a4 via eth0: wrong network.
Jan 21 12:14:35 dnsserver dhcpd: DHCPNAK on to 00:1c:42:0f:bc:a4 via eth0
Jan 21 12:14:35 dnsserver dhcpd: DHCPDISCOVER from 00:1c:42:0f:bc:a4 via eth0
Jan 21 12:14:35 dnsserver dhcpd: DHCPOFFER on to 00:1c:42:0f:bc:a4 (oneiricvm-vm) via eth0
Jan 21 12:14:35 dnsserver named[1567]: client signer "rndc-key" approved
Jan 21 12:14:35 dnsserver named[1567]: client updating zone 'bigdinosaur.org/IN': adding an RR at 'oneiricvm-vm.bigdinosaur.org' A
Jan 21 12:14:35 dnsserver named[1567]: client updating zone 'bigdinosaur.org/IN': adding an RR at 'oneiricvm-vm.bigdinosaur.org' TXT
Jan 21 12:14:36 dnsserver dhcpd: Added new forward map from oneiricvm-vm.bigdinosaur.org. to
Jan 21 12:14:36 dnsserver named[1567]: client signer "rndc-key" approved
Jan 21 12:14:36 dnsserver named[1567]: client updating zone '10.10.10.in-addr.arpa/IN': deleting rrset at '' PTR
Jan 21 12:14:36 dnsserver named[1567]: client updating zone '10.10.10.in-addr.arpa/IN': adding an RR at '' PTR
Jan 21 12:14:36 dnsserver dhcpd: added reverse map from to oneiricvm-vm.bigdinosaur.org.
Jan 21 12:14:36 dnsserver dhcpd: DHCPREQUEST for ( from 00:1c:42:0f:bc:a4 (oneiricvm-vm) via eth0
Jan 21 12:14:36 dnsserver dhcpd: DHCPACK on to 00:1c:42:0f:bc:a4 (oneiricvm-vm) via eth0

I started up a new Ubuntu virtual machine with its networking set to "bridged" so that it would behave like a real LAN host. The VM previously was set to "shared" networking, meaning Parallels (the VM software I'm using) used NAT behind my host computer's LAN IP address. So, the first thing that happens here is that the virtual machine reqests the old NAT'd address Parallels was giving it.

But our DHCP server knows it's authoritative and it can say no, and so it replies with a DHCPNAK packet, telling the client that the address it's requesting is an invalid address for this network. Without the authoritative directive we set above, the client would sit there and stubbornly re-request its old address until its DHCP lease expired, which could take days or months.

Next, having been told its address is invalid, the client sends out a DHCPDISCOVER packet, which the DHCP server sees and replies to with a DHCPOFFER of an IP address out of its range of assignable addresses.

The next step is where the work we did above with dynamic updating will either succeeed or fail. The DHCP server reaches out to the DNS server (referred to in the log as "named", the name server daemon) and presents its credentials in the form of our rndc-key crypto hash. The DNS server finds it valid and approves, and then makes two changes to the Bigdinosaur.org forward lookup zone—it adds an A record for "oneiricvm-vm.bigdinosaur.org", and then it adds a TXT record right below that. The A record is used for forward DNS lookups, and the TXT record contains a signature from the DHCP server so that both the DNS and DHCP server know that the DHCP server was responsible for the creation of this entry. The DHCP server won't modify A records which lack a corresponding TXT record with its signature.

Tthe DHCP server acknowledges in the log that it's successfully added records for our VM into the forward lookup zone. Then, the reverse map is added. Once again, the DNS server validates the DHCP server's update key, and then the DHCP server locates the previous PTR record for that IP address, deletes it, and adds a new PTR record for our VM, then acknowledges that it's made the change.

With that work completed and the client having been told it's allowed to request a valid address via DHCPOFFER, the client then sends a formal DHCPREQUEST packet asking for it to be assigned the address. At the very last step, the DHCP server acknowledges the address's assignment to the VM with a DHCPACK packet. We've done it!

The listing will look a little different for static-mapped hosts or hosts which are merely refreshing their leases, but at all steps you should see this interplay of DNS and DHCP servers in the log.


This has been a ridiculously long entry, but it's also provided an excellent opportunity for me to refresh my own memory of what exactly I did when setting this all up on my LAN last year. The key point to making the DNS and DHCP server work together are the several dynamic update settings and the cryptographic hash generation and exchange; all the other options can be tweaked to fit your own LAN.

Discuss this post on the BigDinosaur forums