djbdns is a software package for running a secure, fast, and simple DNS server.

djbdns is not actually a program itself, but instead is a collection of programs that can be used together to create a full caching, forwarding, and authoritative DNS system - this post will show how to setup all the necessary programs on SmartOS to mimic my home setup.

I use djbdns at home to give me:

  1. DNS lookups for my internal network: rapture.com and 10.X.X.X
  2. DNS caching daemon for quick look ups: it forwards to OpenDNS and caches the results

Installation

To install the suite of tools, run

pkgin in djbdns gmake

Note: the GNU version of make isn't needed specifically, any implementation will suffice.

This will install a lot of programs, but the most important are:

  • tinydns: a DNS server daemon
  • dnscache: a recursive DNS caching daemon

The next step is to create the configuration directory which will be used later

mkdir -p /opt/local/etc/djbdns

Configure tinydns

tinydns is the actual DNS server program - it will read a config file with a list of A records, NS Records, CNAMEs, etc. and provide answers to DNS queries. This program will provide answers for rapture.com and 10.0.0.0/8 while silently discarding all other queries.

The machine we are configuring is named ns1.rapture.com and it has the IP address 10.0.1.2.

First, we create the directory for tinydns configuration

mkdir -p /opt/local/etc/djbdns/tinydns/root

Note: all djbdns programs call chroot(2) for added security, so the root directory will be the directory tinydns locks itself into.

Next, a DNS record file is needed for tinydns called data

cat > /opt/local/etc/djbdns/tinydns/root/data <<-EOF
# NS servers
.rapture.com:10.0.1.2:ns1.rapture.com
.1.0.10.in-addr.arpa:10.0.1.2:ns1.rapture.com

# Forward and Reverse lookups
=ns1.rapture.com:10.0.1.2
=foo.rapture.com:10.0.1.3
EOF

A Makefile is needed to build the database needed by tinydns to run

cat > /opt/local/etc/djbdns/tinydns/root/Makefile <<-EOF
data.cdb: data
        tinydns-data
EOF

Run make to create the database named data.cdb

# cd /opt/local/etc/djbdns/tinydns/root
# make
tinydns-data
# ls
data  data.cdb  Makefile

Finally, start the tinydns server using this SMF manifest

<?xml version='1.0'?>
<!DOCTYPE service_bundle SYSTEM '/usr/share/lib/xml/dtd/service_bundle.dtd.1'>
<service_bundle type='manifest' name='export'>
  <service name='network/tinydns' type='service' version='0'>
    <create_default_instance enabled='true'/>
    <dependency name='dep0' grouping='require_all' restart_on='error' type='service'>
      <service_fmri value='svc:/milestone/multi-user:default'/>
    </dependency>
    <exec_method name='start' type='method' exec='tinydns &amp;' timeout_seconds='10'>
      <method_context working_directory='/opt/local/etc/djbdns/tinydns/root'>
        <method_credential user='root' group='root'/>
        <method_environment>
          <envvar name='PATH' value='/opt/local/gnu/bin:/opt/local/gnu/sbin:/opt/local/bin:/opt/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin:/opt/custom/bin'/>
          <envvar name='IP' value='127.0.0.1'/>
          <envvar name='ROOT' value='/opt/local/etc/djbdns/tinydns/root'/>
          <envvar name='UID' value='60001'/>
          <envvar name='GID' value='60001'/>
        </method_environment>
      </method_context>
    </exec_method>
    <exec_method name='stop' type='method' exec=':kill' timeout_seconds='30'/>
    <template>
      <common_name>
        <loctext xml:lang='C'>tinydns DNS server</loctext>
      </common_name>
    </template>
  </service>
</service_bundle>
# svccfg import manifest.xml
# svcs -p tinydns
STATE          STIME    FMRI
online         Apr_24   svc:/network/tinydns:default
               Apr_24       3870 tinydns

This manifest tells tinydns to:

  1. Start as root, and step down permissions to user 60001 (nobody)
  2. chroot(2) to its root directory
  3. Listen on localhost

We can test that server is working with:

# nslookup foo.rapture.com 127.0.0.1
Server:         127.0.0.1
Address:        127.0.0.1#53

Name:   foo.rapture.com
Address: 10.0.1.3

And lastly, to update the database, you only need to edit the data file and run make, the file will be picked up by tinydns automatically without a service restart.

Configure dnscache

dnscache is the recursive, forwarding, and caching server that will be used to handle requests on the network for any domain name.

Like with tinydns, we create the directories for dnscache configuration

mkdir -p /opt/local/etc/djbdns/dnscache/root/{dev,etc,ip,proc,servers}

./dev and ./proc directory

These directories are mounted using LOFS for the chroot.

First, we add them to the /etc/vfstab file

echo '/dev - /opt/local/etc/djbdns/dnscache/root/dev lofs - yes -' >> /etc/vfstab
echo '/proc - /opt/local/etc/djbdns/dnscache/root/proc lofs - yes -' >> /etc/vfstab

And then mount them

mount /opt/local/etc/djbdns/dnscache/root/dev
mount /opt/local/etc/djbdns/dnscache/root/proc

./etc directory

This directory holds various configuration parameters for dnscache, create the files needed with

cat > /opt/local/etc/djbdns/dnscache/root/etc/netconfig <<-EOF
tcp tpi_cots_ord v inet tcp /dev/tcp -
udp tpi_clts v inet udp /dev/udp -
EOF

and

dd if=/dev/urandom of=/opt/local/etc/djbdns/dnscache/root/etc/seed bs=128 count=1
chmod 0400 /opt/local/etc/djbdns/dnscache/root/etc/seed

./ip directory

This directory contains a series of empty files that tells dnscache what IP addresses are allowed to query it. Since all of my machines are on the 10.x net, all that's needed is:

touch /opt/local/etc/djbdns/dnscache/root/ip/10

./servers directory

This directory contains files that each have a newline separated list of DNS servers to use for certain requests. We need dnscache to send requests to 10.x and rapture.com to localhost (where tinydns is listening), and to forward all others to OpenDNS. To do this, we need to create the following files:

cat > /opt/local/etc/djbdns/dnscache/root/servers/rapture.com <<-EOF
127.0.0.1
EOF
cat > /opt/local/etc/djbdns/dnscache/root/servers/1.0.10.in-addr.arpa <<-EOF
127.0.0.1
EOF
cat > /opt/local/etc/djbdns/dnscache/root/servers/@ <<-EOF
208.67.222.222
208.67.220.220
EOF

./ directory

And finally, the root directory will hold a script simply called run which will be used by SMF to start dnscache:

cat > /opt/local/etc/djbdns/dnscache/root/run <<-EOF
#!/usr/bin/env bash
exec > /dev/null
exec 2> /dev/null
exec < ./etc/seed
exec dnscache
EOF
chmod +x /opt/local/etc/djbdns/dnscache/root/run

Note: in this file I purposefully discard stdout and stderr - I prefer not to log any of the requests made by myself or guests on my network for privacy reasons.

Start the service

Finally, start the dnscache server using this SMF manifest

<?xml version='1.0'?>
<!DOCTYPE service_bundle SYSTEM '/usr/share/lib/xml/dtd/service_bundle.dtd.1'>
<service_bundle type='manifest' name='export'>
  <service name='network/dnscache' type='service' version='0'>
    <create_default_instance enabled='true'/>
    <dependency name='dep0' grouping='require_all' restart_on='error' type='service'>
      <service_fmri value='svc:/milestone/multi-user:default'/>
    </dependency>
    <exec_method name='start' type='method' exec='/opt/local/etc/djbdns/dnscache/root/run &amp;' timeout_seconds='10'>
      <method_context working_directory='/opt/local/etc/djbdns/dnscache/root'>
        <method_credential user='root' group='root'/>
        <method_environment>
          <envvar name='PATH' value='/opt/local/gnu/bin:/opt/local/gnu/sbin:/opt/local/bin:/opt/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin:/opt/custom/bin'/>
          <envvar name='IP' value='10.0.1.2'/>
          <envvar name='ROOT' value='/opt/local/etc/djbdns/dnscache/root'/>
          <envvar name='UID' value='60001'/>
          <envvar name='GID' value='60001'/>
          <envvar name='CACHESIZE' value='1000000'/>
          <envvar name='DATALIMIT' value='3000000'/>
          <envvar name='IPSEND' value='0.0.0.0'/>
          <envvar name='FORWARDONLY' value='1'/>
        </method_environment>
      </method_context>
    </exec_method>
    <exec_method name='stop' type='method' exec=':kill' timeout_seconds='30'/>
    <template>
      <common_name>
        <loctext xml:lang='C'>dnscache DNS server</loctext>
      </common_name>
    </template>
  </service>
</service_bundle>
# svccfg import manifest.xml
# svcs -p dnscache
STATE          STIME    FMRI
online         Apr_24   svc:/network/dnscache:default
               Apr_24       3870 dnscache

This manifest tells dnscache to:

  1. Start as root, and step down permissions to user 60001 (nobody)
  2. chroot(2) to its root directory
  3. Listen on 10.0.1.2
  4. Forward requests only - this means OpenDNS will be queried by dnscache in the event the answer is not already cached

We can test that server is working with:

# nslookup foo.rapture.com 10.0.1.2
Server:         10.0.1.2
Address:        10.0.1.2#53

Non-authoritative answer:
Name:   foo.rapture.com
Address: 10.0.1.3

# nslookup daveeddy.com 10.0.1.2
Server:         10.0.1.2
Address:        10.0.1.2#53

Non-authoritative answer:
Name:   daveeddy.com
Address: 165.225.136.227

Conclusion

  • tinydns: listens on 127.0.0.1 for queries (will be asked by dnscache)
  • dnscache: listens on the external interface for queries and will forward to OpenDNS or 127.0.0.1 depending on domain/ip

Simply point your machines /etc/resolv.conf to 10.0.1.2 and you will have blazing fast DNS on your network.