Doing DNS and DHCP for your LAN the old way—the way that works

Photo of author

By Sedoso Feb


Doing DNS and DHCP for your LAN the old way—the way that works
Enlarge / All shall tremble before your fully functional forward and reverse lookups!
Aurich Lawson | Getty Images

Here’s a short summary of the next 7,000-ish words for folks who hate the thing recipe sites do where the authors babble about their personal lives for pages and pages before getting to the cooking: This article is about how to install bind and dhcpd and tie them together into a functional dynamic DNS setup for your LAN so that DHCP clients self-register with DNS, and you always have working forward and reverse DNS lookups. This article is intended to be part one of a two-part series, and in part two, we’ll combine our bind DNS instance with an ACME-enabled LAN certificate authority and set up LetsEncrypt-style auto-renewing certificates for LAN services.

If that sounds like a fun couple of weekend projects, you’re in the right place! If you want to fast-forward to where we start installing stuff, skip down a couple of subheds to the tutorial-y bits. Now, excuse me while I babble about my personal life.

My name is Lee, and I have a problem

(Hi, Lee.)

I am a tinkering homelab sysadmin forever chasing the enterprise dragon. My understanding of what “normal” means, in terms of the things I should be able to do in any minimally functioning networking environment, was formed in the days just before and just after 9/11, when I was a fledgling admin fresh out of college, working at an enormous company that made planes starting with the number “7.” I tutored at the knees of a whole bunch of different mentor sysadmins, who ranged on the graybeard scale from “fairly normal, just writes his own custom GURPS campaigns” to “lives in a Unabomber cabin in the woods and will only communicate via GPG.” If there was one consistent refrain throughout my formative years marinating in that enterprise IT soup, it was that forward and reverse DNS should always work. Why? Because just like a clean bathroom is generally a sign of a nice restaurant, having good, functional DNS (forward and reverse) is a sign that your IT team knows what it’s doing.

Just look at what the masses have to contend with outside of the datacenter, where madness reigns. Look at the state of the average user’s LAN—is there even a search domain configured? Do reverse queries on dynamic hosts work? Do forward queries on dynamic hosts even work? How can anyone live like this?!

I decided long ago that I didn’t have to, so I’ve maintained a linked bind and dhcpd setup on my LAN for more than ten years. Also, I have control issues, and I like my home LAN to function like the well-run enterprise LANs I used to spend my days administering. It’s kind of like how car people think: If you’re not driving a stick shift, you’re not really driving. I have the same kind of dumb hang-up, but for network services.

Honestly, though, running your LAN with bind and dhcpd isn’t even that much work—those two applications underpin a huge part of the modern Internet. The packaged versions that come with most modern Linux distros are ready to go out of the box. They certainly beat the pants off of the minimal DNS/DHCP services offered by most SOHO NAT routers. Once you have bind and dhcpd configured, they’re bulletproof. The only time I interact with my setup is if I need to add a new static DHCP mapping for a host I want to always grab the same IP address.

So, hey, if the idea of having perfect forward and reverse DNS lookups on your LAN sounds exciting—and, come on, who doesn’t want that?!—then pull up your terminal and strap in because we’re going make it happen.

(Note that I’m relying a bit on Past Lee and this old blog entry for some of the explanations in this piece, so if any of the three people who read my blog notice any similarities in some of the text, it’s because Past Lee wrote it first and I am absolutely stealing from him.)

But wait, there’s more!

This piece is intended to be part one of two. If the idea of having one’s own bind and dhcpd servers sounds a little silly (and it’s not—it’s awesome), it’s actually a prerequisite for an additional future project with serious practical implications: our own fully functioning local ACME-enabled certificate authority capable of answering DNS-01 challenges so we can issue our own certificates to LAN services and not have to deal with TLS warnings like plebes.

(“But Lee,” you say, “why not just use actual-for-real LetsEncrypt with a real domain on my LAN?” Because that’s considerably more complicated to implement if one does it the right way, and it means potentially dealing with split-horizon DNS and hairpinning if you also need to use that domain for any Internet-accessible stuff. Split-horizon DNS is handy and useful if you have requirements that demand it, but if you’re a home user, you probably don’t. We’ll keep this as simple as possible and use LAN-specific DNS zones rather than real public domain names.)

We’ll tackle all the certificate stuff in part two—because we have a ways to go before we can get there.

What are we hosting this DNS business on?

Since we’ll be installing things, we need something on which to install those things. The good news is that bind and dhcpd are extremely lightweight and don’t need a lot of resources to function, so you can run them on pretty much anything.

My choice for exercises like this is an LXC container, which you can spin up on any modern Linux distro (and which works particularly well on Ubuntu Server via LXD). If you’re more old-school and all that containerization talk sounds like gibberish, you might consider a Linux virtual machine to install stuff on. Or keep it physical and try this on a Raspberry Pi or other tiny computer—bind and dhcpd would be happy to run on something like that, and although I haven’t looked, I’m betting there are ARM versions of each available in the repos.

For the screenshots and instructions in this piece, I’ll be using Ubuntu 22.04 LTS—because that’s the latest LTS release available as I type this. Presumably, everything will also be fine with 24.04 LTS if you’re reading this in the future.

Don’t feel like you need to stick with Ubuntu Server for this tutorial just because I’m using it. Use Debian. Use Fedora. Use AlmaLinux. Hell, use Arch if you hate yourself. There are no judgments here. Remember the maxim of the Scary Devil Monastery: All software sucks, all hardware sucks. Pick the thing you’re most comfortable with, get yourself to a bash prompt, and let’s jump in.

But wait! I want to do this with Windows!

Sorry, I’ve got nothing.

Is there a way to get great DNS and DHCP going in a Windows server environment? Absolutely. And if learning about that is your jam, the Googles are right here. Otherwise, for this tutorial, hop aboard the Linux train! It’s fine and not scary at all. You don’t have to compile your own kernel or anything like that—we’ll mostly just be editing text config files. Spin up a Hyper-V instance of Ubuntu LTS and follow along! You won’t get Linux Cooties or anything. I mean, you probably won’t.

Install the things! (If you were skipping ahead, stop here)

All righty, let’s get logged into the box where we’ll do all of this stuff!

Assuming you’re starting out on a fresh Ubuntu 22.04 LTS install, you’ll want to create a non-root user to log in as and then bless that user with the unlimited power of root using visudo. You’ll also want to run an apt update and apt upgrade (or your distro’s equivalent, but from here on out, instructions are Ubuntu-only because I’m lazy) to make sure we’re all up to date before starting.

Make note of the hostname and IP address of the server you’re using! I’d recommend setting it up with a static IP address, and I’d recommend setting the hostname to be dnsbox. (You can name it whatever you want, but this guide is going to assume your server is named dnsbox, so whenever you see dnsbox, replace it with your server’s hostname.) I’ll be using the IP address 192.168.1.55 for dnsbox throughout this piece, so make sure to substitute in your own dnsbox‘s IP address whenever you see 192.168.1.55.

With that out of the way, let’s start installing:

$ sudo apt install bind9 isc-dhcp-server
Installing away!
Enlarge / Installing away!
Lee Hutchinson

DNS should be running immediately, but the DHCP server in its default Ubuntu configuration isn’t set up to listen on any IPv4 interfaces and won’t actually start. And that’s fine, because we don’t want it to listen until we have it configured anyway. (I’ll confusingly refer to the DHCP server as dhcpd throughout, even though the package it comes from is called isc-dhcp-server.)

Get DNS operational

Let’s focus first on bind, or named (that’s pronounced “name dee,” as in “the name daemon,” not “named” as in past tense). Being a good *nix citizen, bind keeps its configuration files in /etc/bind:

The contents of /etc/bind just after install.
Enlarge / The contents of /etc/bind just after install.
Lee Hutchinson

There are three files we’re particularly interested in: named.conf, named.conf.options, and named.conf.local. The first file links the whole config together; the .options file is where you should set bind’s configuration options; and the .local file is where we’ll specify the zone(s) for which our bind instance will serve queries.

First, pop open named.conf in your favorite editor. The prepackaged version of the file included with the Ubuntu 22.04 LTS distro doesn’t have much in it:

The named.conf file. Kinda empty in here.
Enlarge / The named.conf file. Kinda empty in here.
Lee Hutchinson

We’ll begin our additions here. First, we’ll define an access control list named internal-net that points at our internal network. This ACL will be our shorthand by which we can refer to trusted addresses we want to be able to use our bind server. Make this addition above the include statements so that the ACL has already been instantiated when the other config files are parsed for inclusion (since those files will eventually reference this ACL once we get to editing them):

acl internal-net { localhost; 192.168.1/24;
};

This assumes you’re using 192.168.1.0/24 as your LAN segment. I use 10.10.10.0/24 for my trusted LAN segment, and I have a couple of other 10.10.x.0/24 segments for other purposes that I also want to be able to query DNS, so my internal-net ACL looks like this, instead:

acl internal-net { localhost; 10.10/16;
};

You’ll want to tune your ACL as appropriate so that it points to the right network segment or segments.

Once you have your ACL in, add one more section below that to make sure the server ignores queries coming in over IPv6:

server ::/0 { bogus yes;
};

This will make sure that bind knows that traffic coming in over any IPv6 interface should be ignored. (If you want to set this up with IPv6, go right ahead! Unfortunately, Frontier currently doesn’t do IPv6 in my neck of the woods. Because I don’t have IPv6 configured locally and also want to keep this guide from ballooning even bigger than it already is, I’m going IPv4-only.)

With those changes in, save named.conf. Sans comments, it should now look something like this, with your IP address range specified in the ACL:

acl internal-net { localhost; 192.168.1/24;
}; server ::/0 { bogus yes;
}; include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.local";
include "/etc/bind/named.conf.default-zones";

Now let’s start configuring things in named.conf.options. Again, the packaged version of the file that comes with Ubuntu 22.04 LTS is pretty minimal:

Starting out with named.conf.options.
Enlarge / Starting out with named.conf.options.
Lee Hutchinson

We’ll leave directory "/var/cache/bind"; alone, but let’s comment out the other two lines. First, strike dnssec-validation auto—it’s unnecessary since we won’t be messing with DNSSEC. Then strike listen-on-v6 { any; }, which is unnecessary because we’re sticking with IPv4 only.

Now add the following four lines below the big block of comments but before the final closing } brace:

allow-query { internal-net; };
allow-query-cache { internal-net; };
allow-recursion { none; };
allow-transfer { none; };

Here’s the breakdown on those four lines:

  • allow-query { internal-net; }; tells bind to only allow DNS queries from the internal-net ACL we previously set.
  • allow-query-cache { internal-net; }; tells bind to only return cached query results to queries coming from the internel-net ACL.
  • allow-recursion { none; }; tells bind to not do recursive lookups for anyone—in other words, this server should only be returning results for hosts over which it is authoritative. This is not intended to be a forwarding or fully recursive resolver (it could be either if you want it to, but that’s beyond the scope of this guide).
  • allow-transfer { none; }; tells bind not not allow any other DNS server to request a zone transfer. If you want to have multiple redundant DNS servers for your LAN, you’d likely want this enabled for an ACL that corresponds to the addresses of your DNS servers so they can do DNS server stuff to each other.

Below this, add the following three entries:

check-names master ignore;
check-names slave ignore;
check-names response ignore;

The check-names directive tells bind how to respond when its lookups return results that might clash with RFC standards—most often, this happens when a hostname has an underscore in it. Although IETF RFC 2181 is the current source of standardization for this kind of stuff, the question of what makes a valid hostname is more religious debate than anything else, and setting those three check-names options forces bind to shut up and deal with whatever hostname format we choose to use in every possible lookup context.

Finally, add this:

notify no;

The notify directive generates notifications between DNS servers when zone files are modified (among other things), and since we only have the one server, we can turn this off to keep some unnecessary verbosity off the wire and out of the log files.

The named.conf.options file after these modifications should look like this, without the comments:

options { directory "/var/cache/bind"; allow-query { internal-net; }; allow-query-cache { internal-net; }; allow-recursion { none; }; allow-transfer { none; }; check-names master ignore; check-names slave ignore; check-names response ignore; notify no;
};

Get in the zone

Now comes the heavy lifting. We’ve configured bind, and now we’ll configure bind’s zones. In DNS-speak, a “zone” is a chunk of namespace managed by a DNS server, and for our purposes, we’re going to end up defining a pair of the things.

To stick with best practices, we define our zones in the named.conf.local file, which by default should be empty except for some comments:

Not much in named.conf.local to start with, either.
Enlarge / Not much in named.conf.local to start with, either.
Lee Hutchinson

Now is the time to decide what you want your LAN’s DNS name to be. I’m using bigdinosaur.lan for my LAN, which complements my main external domain bigdinosaur.org. For this example, we’re going to use crazypants.lan because it sounds fun. We got crazy pants! We got crazy pants in this LAN! Note that you don’t have to pick crazypants.lan if you’re boring or afraid of excitement—you can pick whatever you want, like beige.lan or lame.town or nothingcooleverhappensonthisnetwork.butt. There’s no wrong answer, although you might eventually get stuck having to explain your network name to curious friends, spouses, or children.

And about that TLD, another big question to ask yourself is whether you want to configure a LAN DNS zone using an existing top-level domain like .com or .org, or with a non-existent TLD like .mine or .lan (or, if you want to follow ICANN’s recommendations as of January 2024, .internal). If you have a domain already registered, you can use it, but as noted at the beginning of this piece, you will likely then enter the world of split-horizon DNS and/or hairpin NAT if you have domain resources you want to access both on your LAN and on the Internet. Doing so is beyond the scope of this guide, though.

So for this tutorial, we’re keeping it simple with crazypants.lan. All crazy, all pants, all LAN, all the time. Append the following to the bottom of named.conf.local:

zone "crazypants.lan" { type master; file "/var/lib/bind/crazypants.lan.hosts";
};

Then—you guessed it—we need to build the referenced zone file. Create a new file named crazypants.lan.hosts in /var/lib/bind/ and make it look like this:

$ORIGIN .
$TTL 907200 ; 1 week 3 days 12 hours
crazypants.lan IN SOA dnsbox.crazypants.lan. webmaster.crazypants.lan. ( 1263529355 ; serial 10800 ; refresh (3 hours) 3600 ; retry (1 hour) 604800 ; expire (1 week) 38400 ; minimum (10 hours 40 minutes) ) NS dnsbox.crazypants.lan.
$ORIGIN crazypants.lan.
gateway A 192.168.1.1
dnsbox A 192.168.1.55
test-server1 A 192.168.1.60
test-server2 A 192.168.1.61
test-server3 A 192.168.1.62

Replace the 192.168.1.55 IP address with the IP address of your DNS server or VM that you’re currently logged into. With that done, let’s take this file apart and see what everything means.

The first bit, $ORIGIN ., indicates that the thing we’re configuring here—that is, the zone crazypants.lan—has its origin at the “.” domain. The “.” domain is the root from which all TLDs branch.

Below that, $TTL 907200 sets the default record time-to-live setting of 907,200 seconds (1 week, 3 days, 12 hours). Unless explicitly overridden, all DNS records in this zone will have this TTL.

The next set of lines are the “Start of Authority” (SOA) record and define some basic info about your domain. Here, we’re saying that the domain name for this zone file is crazypants.lan, that dnsbox.crazypants.lan. (with period!) is the source host for crazypants.lan, and that webmaster.crazypants.lan. is the zone maintainer. (You use a dot instead of an @ here because that’s how zone file syntax works.) The lines after that define the zone’s serial number, which is used to keep track of when the zone file was last modified, and then some interval definitions that we can leave at default.

After that, we’ve got a record of type NS, which defines the hostname of the name server primarily responsible for serving this zone file (dnsbox.crazypants.lan., with period!).

Finally, below all that, we have another $ORIGIN statement, except this one lists $ORIGIN crazypants.lan. (with trailing period!). This indicates that hostnames below that line do not originate from “.”, but from crazypants.lan—meaning that bind will understand that the first A-record for dnsbox actually refers to dnsbox.crazypants.lan. And speaking of dnsbox, the first A-record defines dnsbox‘s IP address.

Below the dnsbox A-record is where you add your own A-records for any statically addressed hosts. All you need in order to add an A-record is the hostname and the IP address. For example, if you wanted to add an A-record for a statically addressed host named mysql.crazypants.lan at IP address 192.168.1.21, you’d add this below dnsbox‘s A-record:

mysql.crazypants.lan	A	192.168.1.21

This is a good time to add in any static hosts you have on your LAN, including your router or gateway, so go ahead and fill ’em in. Don’t worry about adding anything for DHCP clients—by the time we’re done with this tutorial, the server will be configured such that DHCP clients will add themselves to the zone file as they grab their leases. This will be totally awesome.

Reverse zones

We’ve defined our forward lookup zone—that is, we’ve defined a zone to answer DNS queries for hostnames in the crazypants.lan domain. However, we have to define a whole separate zone in order for reverse lookups to function. To do that, let’s go back to named.conf.local and add another zone definition:

zone "1.168.192.in-addr.arpa" { type master; file "/var/lib/bind/1.168.192.rev";
};

We need to use a specific naming format for reverse-lookup zones—hence the “in-addr.arpa” bit. If your LAN segment uses 192.168.1.0/24 like most folks, then you want to name your reverse zone 1.168.192.in-addr-arpa like in the example. If your LAN segment uses 10.10.10.0/24 like me, you’d use 10.10.10.in-addr-arpa. (You 172-dot-whatever crazy people can figure out your own in-addr.arpa names. Ain’t nobody got time for that.)

Once you’ve added the reverse zone, create its zone file in /var/lib/bind/ with the appropriate file name and populate it thusly:

$ORIGIN .
$TTL 907200 ; 1 week 3 days 12 hours
1.168.192.in-addr.arpa IN SOA dnsbox.crazypants.lan. webmaster.crazypants.lan. ( 1263189277 ; serial 10800 ; refresh (3 hours) 3600 ; retry (1 hour) 604800 ; expire (1 week) 38400 ; minimum (10 hours 40 minutes) ) NS dnsbox.crazypants.lan.
$ORIGIN 1.168.192.in-addr.arpa.
1 PTR gateway.crazypants.lan.
55 PTR dnsbox.crazypants.lan.
60 PTR test-server1.crazypants.lan.
61 PTR test-server2.crazypants.lan.
62 PTR test-server3.crazypants.lan.

This looks more or less the same as the forward lookup zone, except for the kinds of records we’re adding below the second $ORIGIN statement. The reverse-lookup zone contains PTR (“pointer”) records, which do the inverse of A-records and correlate an IP address to a hostname. (Technically, they correlate an in-addr.arpa hostname to a LAN hostname via the magic of IETF RFC 5855, but we’re going to stick with the simple explanation.)

For every statically addressed A-record entry that you put in the forward lookup zone file, this is the place where you should add a matching PTR entry to make reverse entries for your static hosts work. Definitely make sure dnsbox has one. As before, don’t worry about your DHCP hosts—they’ll be magically registering themselves before you know it.

With the majority of the bind work out of the way, let’s do some quick file system cleanup. If your version of bind was installed by a package manager—which, if you’re following this guide, it was—you’ll want to make sure everything in /etc/bind and /var/lib/bind is owned by the bind user. Otherwise, bind may not be able to see its configuration files, and things will break.

Set the ownership right with these two commands:

$ sudo chown -R bind:bind /etc/bind/
$ sudo chown -R bind:bind /var/lib/bind/

Finally, the last thing we want to do is verify that our bind configuration files don’t have any typos in them. Use the named-checkconf command to verify everything looks OK:

$ sudo named-checkconf -jlz /etc/bind/named.conf

You should get some output that looks like this, indicating that the configuration files parsed OK and that your zones are configured without any obvious errors:

Lint successful.
Enlarge / Lint successful.
Lee Hutchinson

Let’s see if DNS works!

Let’s bounce the bind service with systemctl restart bind9, which will force it to pick up all of our configuration changes. If you get any errors on restarting the service, carefully check the configuration and zone files for tiny typos—a single missing period in a zone file once set me on a day-long troubleshooting spiral. Don’t be me—check your work.

If bind restarts without any obvious errors, peek in the syslog file (try cat /var/log/syslog |grep -i named to just get entries for bind, which if you’ll remember is also referred to as the named service) and see if it’s reporting that all its zone files are properly configured. You’re looking for log entries like this:

Feb 5 11:01:49 dnsbox named[3741]: zone 127.in-addr.arpa/IN: loaded serial 1
Feb 5 11:01:49 dnsbox named[3741]: zone localhost/IN: loaded serial 2
Feb 5 11:01:49 dnsbox named[3741]: zone crazypants.lan/IN: loaded serial 1263529355
Feb 5 11:01:49 dnsbox named[3741]: zone 0.in-addr.arpa/IN: loaded serial 1
Feb 5 11:01:49 dnsbox named[3741]: zone 255.in-addr.arpa/IN: loaded serial 1
Feb 5 11:01:49 dnsbox named[3741]: zone 1.168.192.in-addr.arpa/IN: loaded serial 1263189277
Feb 5 11:01:49 dnsbox named[3741]: all zones loaded
Feb 5 11:01:49 dnsbox named[3741]: running

In particular, you want to make sure your crazypants.lan forward zone and your in-addr.arpa reverse zone are showing a status of loaded.

If they are, we can actually issue a query and see if bind gives us back an answer. You should already have at least one A-record in the forward zone and a corresponding reverse pointer in the reverse zone, so we’ll query both with the dig command and see what happens.

First, let’s try a forward lookup. Run the following command:

$ dig @localhost dnsbox.crazypants.lan +short

(Only use dig @localhost if you’re logged into the DNS server and running commands locally. If you’re logged into a different computer, use dig @192.168.1.55 or whatever the DNS server’s IP address is.)

If everything is working OK, that dig command should yield one line of output showing the IP address for dnsbox. If everything is not OK, it probably won’t produce any output, and you’ll need to peek at /var/log/syslog to see what, if any, errors are showing up.

If the forward lookup completes successfully, we can try a reverse lookup, substituting in your DNS server’s IP address:

$ dig @localhost -x 192.168.1.55 +short

If everything works, you should get a single line of output with the DNS server’s hostname. As above, if you don’t get that, head to /var/log/syslog and see if bind is reporting any obvious errors.

Here’s what you should see if everything looks OK:

Forward lookup, check! Reverse lookup, check!
Enlarge / Forward lookup, check! Reverse lookup, check!
Lee Hutchinson

And with that…holy crap, we have LAN-only DNS.

Last DNS bit: How to plug bind into your existing DNS setup

Before we move on to DHCP, there is one hugely important question to address: How do we actually send queries to this DNS server? You’re reading these words in a web browser into which you presumably typed https://arstechnica.com at some point, so your LAN almost certainly has some manner of DNS resolver already functioning. How do you hook that existing resolver up to this new bind instance we’ve created so that our new bind instance handles LAN DNS lookups?

The first and easiest option—the one I use—is to configure your existing DNS resolver to forward queries for your LAN’s domain and IP addresses to our new bind instance. If you’re using an existing solution that relies on DNSMasq—something like pfsense or a PiHole—you can modify that solution’s DNSMasq configuration with a couple of lines. I like to create a new file in /etc/dnsmasq.d called “00-localdns.conf“, and populate it thusly:

server=/crazypants.lan/192.168.1.55
rev-server=192.168.1.0/24,192.168.1.55

Then, when my local resolver gets any queries for the LAN domain or for the LAN range of addresses, those queries are forwarded on to my bind instance and answered there. Magic!

The other path to take is to transform our bind instance into a full DNS server that answers queries not just about your LAN hosts but about all hosts. You can do this either by adding forwarding entries in named.conf.options, which will make your DNS server into a caching forwarder, or by enabling recursive lookups by modifying the allow-recursion setting in the same file from none to internal-net, which will make your DNS server into a recursive resolver. That means bind will do full recursive queries when you ask it to look up hosts from zones it doesn’t control. (However, please, please, please note that you should investigate some bind configuration best practices before simply enabling recursive lookups and walking off. At the very least, you should read up on how to configure qname minimization to help with the privacy of your lookups.)

Let’s get our dhcpd on

Sick of bind yet? No worries—it’s time to turn our attention to the other application we installed: isc-dhcp-server, or just “dhcpd” to its friends.

Going in, we need to be just a liiiiiiitle bit careful with our configuration because unlike DNS, where we can have our LAN-only DNS server coexisting with our currently functioning DNS resolver while we do our setup, DHCP works a bit differently. DHCP is a broadcast protocol, and that means having two DHCP servers that don’t know about each other both operating on the same network segment can create what computer people like to euphemistically call “nondeterminstic behavior,” which is bad. When we’re done with this tutorial, the intention is to have this new DHCP server we’re configuring replace your existing DHCP server.

So there are two options before proceeding. First, and probably safest, is to configure your new DHCP server to listen on a totally different made-up network segment while we get everything configured and then cut over from old server to new server when we’re sure everything is safe and configured. (If you’re following along with virtual machines or containers, shoving them all off on their own little isolated network segment should be pretty easy, and that’s a wise choice for this kind of project.)

The second option, which is less safe but way easier, is to disable your existing DHCP setup before bringing this one online. (The reason this method is less safe is that if something gets broken or doesn’t work right, you’ll be left without functional DHCP until you kill the new one and bring the old one back online. But hey, testing in production makes the world go ‘round, amiright?!)

So pick which method you’re going to do. For this guide, I’ll assume there are no other operational DHCP servers, so I’ll be using “real” IP addresses. If you want to set dhcpd up on a test segment first—which, again, might be wise—substitute in your own test IP addresses and swap them to the real ones when you’re confident DHCP is working.

Basic configuration

The dhcpd service keeps its configuration isolated in a single file: /etc/dhcp/dhcpd.conf. We’re going to replace the entire contents of that file, so you can move it out of the way (I usually do something like mv dhcpd.conf dhcpd-orig.conf), and pop open a new instance of dhcpd.conf for editing.

In that file, let’s paste the following first:

authoritative;
allow unknown-clients;
ddns-update-style none;
use-host-decl-names on;
default-lease-time 1814400; #21 days
max-lease-time 1814400; #21 days
log-facility local2;

And the breakdown:

  • authoritative; tells dhcpd to act as the one true DHCP server for the DHCP scopes it’s configured to understand, by sending out DHCPNAK (“DHCP no acknowledge”) packets to misconfigured DHCP clients.
  • allow unknown-clients; tells dhcpd that it can assign DHCP leases to clients without static host declarations, which is almost certainly something you want. Otherwise, only hosts with static DHCP mappings will be serviced by dhcpd.
  • ddns-update-style none; Don’t attempt to update DNS when a DHCP lease is set. We’re going to come back to this setting shortly!
  • use-host-decl-names on; tells dhcpd to tell static-mapped clients what their hostname is via the “hostname” option inside the DHCP response. This is a legacy option 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 define how long your DHCP leases are good for. On a small home LAN, these numbers don’t matter that much.
  • log-facility local2; sets local logging at a useful level.

Now that we’ve given dhcpd some basic configuration, we’ll define our primary dhcp domain and its scope. In dhcpd-speak, a “scope” refers to a consecutive range of IP addresses that dhcpd can assign out to clients.

Assuming your home LAN utilizes a single /24 segment, I like to reserve the double-digit numbers for statically assigned hosts and let the DHCP server have the three-digit numbers. That way, I always know that any LAN host with a 1- or 2-digit final octet is statically assigned, and anything with a 3-digit final octet is dynamic. We’re going to implement this strategy here, so append this to the bottom of your dhcpd.conf file:

# crazypants.lan main subnet
subnet 192.168.1.0 netmask 255.255.255.0 { range 192.168.1.100 192.168.1.254; option subnet-mask 255.255.255.0; option routers 192.168.1.1; option broadcast-address 192.168.1.255; option domain-name-servers 192.168.1.55; option domain-name "crazypants.lan";
}

The first couple of lines establish that we’ll be handing out DHCP addresses on the 192.168.1.0/24 segment and that dhcpd will use the range of 192.168.1.100 through 192.168.1.254 for dynamic addresses for hosts.

The next lines contain information that dhcpd will hand out to clients along with IP addresses. Specifically, option subnet-mask sets the client’s subnet mask (surprise!); option routers hands out the LAN gateway; broadcast-address sets the broadcast address for the segment; domain-name-servers tells clients what the primary DNS server for the domain is; and finally, option domain-name tells clients what their local domain and primary search domain should be set to.

You’ll want to make sure that these are all correct, but especially option domain-name-servers, which you absolutely want pointing to your LAN’s primary DNS server. DHCP clients will otherwise find name resolution difficult.

Let them eat static

One of my favorite features to abuse is static DHCP mapping, which gives you most of the benefits of static IP address assignment without the pain. All you need to know is the MAC address of the host you want to give a static assignment to, and then you add that in a group below the subnet we just defined:

group { # my phone host leephone.crazypants.lan { hardware ethernet 12:34:56:ab:cd:ef; fixed-address 192.168.1.70; ddns-hostname "leephone"; }
}

You can add as many static assignments as you’d like (just put them before the closing bracket for group). Notably, static assignments do not have to come from the DHCP scope you defined above—in our example, I’m giving leephone.crazypants.lan a static assignment of 192.168.1.70, even though I’ve defined my DHCP scope to go from 192.168.1.100 to 192.168.1.254. When you’re done, the config file should look more or less like this:

This is how dhcpd.conf should look. My config in the screenshots is using 10.10.10.0/24—make sure you're using the appropriate segment for your LAN!
Enlarge / This is how dhcpd.conf should look. My config in the screenshots is using 10.10.10.0/24—make sure you’re using the appropriate segment for your LAN!
Lee Hutchinson

Hit it with the following command to verify that the configuration is valid and without typos:

$ dhcpd -t

If everything is OK, the command will exit with status 0. If there’s something wrong, it will throw a status other than 0, and you can consult /var/log/syslog |grep -i dhcpd to see what’s wrong.

Testing out dhcpd

Assuming that everything is OK and your configuration is valid, the only thing left to do is (re)start the isc-dhcp-server service. If you have another DHCP server on your LAN (like the one running on your SOHO NAT router!), make sure to disable it first or Weird Stuff™ can happen.

Fire up dhcpd:

$ sudo systemctl start isc-dhcp-server.service

Once it’s up, use tail to monitor the syslog file so you can watch what dhcpd is doing:

$ sudo tail -f /var/log/syslog |grep -i dhcpd

And to actually test it, release and renew a DHCP lease from one of your network devices. You could use your phone, a laptop, or whatever. If DHCP is working properly, you’ll see log output that will look something like this:

Feb 5 12:59:48 dnsbox dhcpd[4954]: DHCPREQUEST for 10.10.10.106 from ab:cd:ef:12:34:56 via eth0
Feb 5 12:59:48 dnsbox dhcpd[4954]: DHCPACK on 10.10.10.106 to ab:cd:ef:12:34:56 (lee-laptop) via eth0
Feb 5 13:00:07 dnsbox dhcpd[4954]: reuse_lease: lease age 19 (secs) under 25% threshold, reply with unaltered, existing lease for 10.10.10.106

That’s my laptop, sticking with the familiar and stubbornly refusing to try a new lease (dhcpd honors the laptop’s DHCPREQUEST to keep its existing address). You may see a longer back-n-forth if you have dhcp clients that are less picky about holding onto their leases.

Form the DNS + DHCP Voltron!

Now comes the fun part—now we combine the power of DHCP with DNS and enable dynamic DNS updates.

This kind of dynamic DNS is perhaps slightly different from what you might commonly think of when you hear “dynamic dns” or “ddns.” Instead of constantly updating a remote authoritative DNS server with our home IP address, this form of dynamic DNS updates the zone files (forward and reverse) of our local DNS instance with the names and IP addresses assigned out by dhcpd. This is the trick to always having fully functional forward and reverse DNS—dynamic updates from dhcpd. Making this work requires a tiny bit of cryptographic key generation and some configuration modification to both the bind and dhcpd configuration files. Let’s get to it!

We must first create a cryptographic key that bind and dhcpd will share so that bind will be able to allow dhcpd to dynamically update the bind zone files. We do this with a handy-dandy little tool called rndc-confgen, which should already be on your server:

$ sudo /usr/sbin/rndc-confgen -a

This ought to spit out a message saying wrote key file /etc/bind/rndc.key, which means it worked. Let’s look at that key file, because we’ll use it in just a second:

$ sudo cat /etc/bind/rndc.key
key "rndc-key" { algorithm hmac-sha256; secret "big ol' string";
};

Except yours won’t say “big ol’ string” and will instead have a big ol’ string of letters and numbers.

We need bind to be able to see this file, so the first thing is to edit named.conf and add an include statement so the contents of the rndc.key file show up in your bind configuration. When done, named.conf ought to look like this:

acl internal-net { localhost; 192.168.1/24;
}; server ::/0 { bogus yes;
}; include "/etc/bind/rndc.key";
include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.local";
include "/etc/bind/named.conf.default-zones";

Then, after it’s in there, save out of named.conf and open up named.conf.local. We’re going to add an allow-update line to each zone definition to allow zone updates utilizing that key. The file should look like this when you’re done:

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

Restart your bind service to make those changes live (make sure to check the syslog for any errors!)—and that’s it for bind. It’s now listening on a separate command channel for other services to request DNS zone file updates with that cryptographic key.

Now for dhcpd. First, let’s copy our key file into the DHCP directory and adjust the permissions so dhcpd can use it:

$ sudo cp /etc/bind/rndc.key /etc/dhcp/rndc.key
$ sudo chown root:dhcpd /etc/dhcp/rndc.key

Open up /etc/dhcp/dhcpd.conf and make the following additions to the first big block of parameters:

include "/etc/dhcp/rndc.key";
ddns-updates on;
update-static-leases on;
ddns-update-style interim;

(We’d previously set ddns-update-style to none, so make sure to remove that entry and replace it with the one above.)

Then we need to add a section telling dhcpd which DNS zones it will be performing updates in and what key to use. Make these additions directly below the log-facility local2 line:

# crazypants.lan forward lookup segment
zone crazypants.lan. { primary localhost; key rndc-key;
} #crazypants.lan reverse lookup segment
zone 1.168.192.in-addr.arpa. { primary localhost; key rndc-key;
}

Finally, we need to two additions to the crazypants.lan subnet definition right below. Add these lines underneath the option domain-name-servers definition:

ddns-domainname "crazypants.lan.";
ddns-rev-domainname "in-addr.arpa.";

The entire dhcpd.conf file should look something like this when done:

include "/etc/dhcp/rndc.key";
ddns-updates on;
ddns-update-style interim;
update-static-leases on;
authoritative;
allow unknown-clients;
use-host-decl-names on;
default-lease-time 1814400; #21 days
max-lease-time 1814400; #21 days
log-facility local2; # crazypants.lan forward lookup segment
zone crazypants.lan. { primary localhost; key rndc-key;
} #crazypants.lan reverse lookup segment
zone 1.168.192.in-addr.arpa. { primary localhost; key rndc-key;
} # crazypants.lan main subnet
subnet 192.168.1.0 netmask 255.255.255.0 { range 192.168.1.100 192.168.1.254; option subnet-mask 255.255.255.0; option routers 192.168.1.1; option broadcast-address 192.168.1.255; option domain-name-servers 192.168.1.2; option domain-name "crazypants.lan"; ddns-domainname "crazypants.lan."; ddns-rev-domainname "in-addr.arpa.";
} group { (static assignments continue below...)

Test your dhcpd config file one last time and restart dhcpd to set the configuration live. Then cross all of your fingers and toes—let’s try a DHCP lease renewal and see what happens. Get your sudo tail -f /var/log/syslog going, grab a phone or a laptop or something, and force that thing to release and renew its DHCP lease.

Assuming we didn’t have any typos and all the pieces are hooked together properly and working right, what you’ll observe in syslog is a neat collaborative dance between named and dhcpd that looks something like this:

Feb 5 14:53:08 dnsbox dhcpd[7079]: DHCPREQUEST for 10.10.10.105 from 12:34:56:ab:cd:ef via eth0
Feb 5 14:53:08 dnsbox dhcpd[7079]: DHCPACK on 10.10.10.105 to 12:34:56:ab:cd:ef (Microraptor) via eth0
Feb 5 14:53:08 dnsbox named[7096]: client @0x7e11fee0e608 127.0.0.1#54583/key rndc-key: signer "rndc-key" approved
Feb 5 14:53:08 dnsbox named[7096]: client @0x7e11fee0e608 127.0.0.1#54583/key rndc-key: updating zone 'crazypants.lan/IN': adding an RR at 'Microraptor.crazypants.lan' A 10.10.10.105
Feb 5 14:53:08 dnsbox named[7096]: client @0x7e11fee0e608 127.0.0.1#54583/key rndc-key: updating zone 'crazypants.lan/IN': adding an RR at 'Microraptor.crazypants.lan' TXT "3166a119dd4c677aa24617f6347828a026"
Feb 5 14:53:08 dnsbox dhcpd[7079]: Added new forward map from Microraptor.crazypants.lan. to 10.10.10.105
Feb 5 14:53:08 dnsbox named[7096]: client @0x7f29fc1eaa78 127.0.0.1#43355/key rndc-key: signer "rndc-key" approved
Feb 5 14:53:08 dnsbox named[7096]: client @0x7f29fc1eaa78 127.0.0.1#43355/key rndc-key: updating zone '10.10.10.in-addr.arpa/IN': deleting rrset at '105.10.10.10.in-addr.arpa' PTR
Feb 5 14:53:08 dnsbox named[7096]: client @0x7f29fc1eaa78 127.0.0.1#43355/key rndc-key: updating zone '10.10.10.in-addr.arpa/IN': adding an RR at '105.10.10.10.in-addr.arpa' PTR Microraptor.crazypants.lan.

(Your IP addresses will obviously be different from mine!)

We can see the DHCPREQUEST come in from a client identified by its MAC address, and the server acknowledges the request with a DHCPACK. Then, using the key we generated, bind adds an A-record for microraptor.crazypants.lan (the client’s fully qualified name) containing the IP address assigned by dhcpd. It also adds a second kind of record called a TXT record. TXT records can be used for all kinds of stuff, and here, it contains a hash so that bind knows that particular entry was one added by itself (and is therefore an entry that bind knows it can update or delete as needed). After this, dhcpd reports back that a forward lookup has been added for the client, and the dynamic forward map has been added.

Then, immediately below that, the same process happens for the reverse zone. Bind notices that there’s already a PTR record for this particular client in the reverse lookup zone, so it deletes it and adds a new PTR. Then dhcpd reports back that a dynamic reverse map has been successfully added.

Let’s go see. Stop the bind service to force it to write out any pending changes in its journal, and then let’s peek at these files to see what got written. Here are my forward and reverse zone files from /var/lib/bind, after some dynamic additions:

Apparently, in the maybe 10 minutes I had the crazypants.lan server up and functional to do my config validation and copy the log files, several LAN devices renewed their leases—I see my laptop, my watch, and my phone in there. Independent setup validation! Success!

It works! Hooray! So, what have we accomplished?

We have accomplished four things, each of which is extremely important.

First, we now have a fully functional caching LAN DNS resolver, courtesy of bind. It can be transformed from just a caching LAN DNS resolver to a full forwarding or recursive resolver with trivial effort, or you can do what I do and use it as an upstream resolver for another DNS server by forwarding your LAN lookup traffic to it. (And there’s a short configuration snippet for how to make this work with pfsense or a PiHole or other DNSMasq-utilizing tool buried somewhere above in the text.)

Secondly, we now have an endlessly configurable and totally reliable DHCP setup, with the ability to quickly add static mappings by editing a single config file.

Thirdly, our DNS and DHCP setup has been linked together so that we have always-working forward and reverse dynamic DNS lookups for all LAN hosts with DHCP-assigned addresses. This was ostensibly the goal of this entire exercise, and we made it. And other than adding static DHCP entries, you’ll never have to mess with it.

Finally, having that local LAN DNS satisfies the prerequisites for really interesting follow-on activities, which brings us to the end of part one and the tease for part two.

Coming soon, part two: A private LetsEncrypt for our LAN

In a bit, we’ll follow up this weekend project piece with a second weekend one covering setting up a local certificate authority capable of issuing ACME challenges and automating LAN client certificate renewal so you don’t ever have to see a crappy self-signed certificate error ever again.

We’ll do this primarily with the ACME dns-01 challenge, which requires certificate requesters to set some temporary records in a DNS zone file to demonstrate ownership. And since we already have a nice and accessible DNS zone file for our LAN, it’s not that complicated a journey to go from where we are now to auto-certificate-renewing bliss.

Stay tuned, folks, and keep your keyboards sharp. We’ll be back!

Source

Leave a Comment

WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE WCE