Multi-Domain DHCP

Well, I’ve managed to knock down one of the tasks that’s been on my “TODO” list for a while, thanks to finding someone building PDNS debs from git.

Background

I live with another geek. When we moved in together, both of us had our own domains, with DHCP servers giving out addresses for the computers we own. As we brought the computers together, the easiest solution was to run as normal, but turn off the “authoritative” function of the two DHCP servers. By doing that, both servers see the DHCP request; one will not know the host, so won’t respond, allowing the other one to supply an address. It was a little clunky but it worked.

Now, moving to a single DHCPd setup is quite a bit neater, but the problem comes with maintaining domains. The task ahead of us is to somehow declare that Machine A belongs to Domain A, so does Machine B, but Machine C belongs to Domain B… For some machines (such as guests) it doesn’t matter which domain they belong to.

The traditional answer to this is to use Classing and Subclassing.

Classes

In dhcpd.conf, it’s possible to assign a host to a class. Assignment to a class is done using the match keyword, and the typical use of that is match hardware (i.e. the MAC address of the host). Clever things can be done such as substring’ing the hardware to put all devices from a certain manufacturer (say, VOIP phones or diskless workstations) into a class. This class can then be used
to determine membership of a pool.

For example, here we declare two classes, “red-hosts” and “blue-hosts” and then use subclasses to extend the classes based on the MAC address of some hosts;

class "red-hosts" {
    match hardware;
}

class "blue-hosts" {
    match hardware;
}

host "alpha" {
    hardware ethernet 08:00:2b:4c:39:ad;
}
subclass "red-hosts" 1:08:00:2b:4c:39:ad;

host "beta" {
    hardware ethernet 08:00:2b:a9:cc:e3;
    fixed-address 192.168.99.99;
}
subclass "blue-hosts" 1:08:00:2b:a9:cc:e3;

host "gamma" {
    hardware ethernet 00:00:c4:aa:29:44;
}
subclass "red-hosts" 1:00:00:c4:aa:29:44;

pool {
    allow members of "red-hosts";
    range 192.168.100.10 192.168.100.200;
}

The “1” at the start of the hardware address in the subclass statement indicates that this is an ethernet address.

Now, the layout shown above is how you’ll see most examples around the internet performing subclassing. You declare a host (along with any host-specific parameters) and then you add it to a class by subclassing.

The main issue I had with this plan was simply that, for each host, you have to write in the hardware address twice. I figured, isn’t there a way in which I only need to declare a host once and then mark it as being in the class?

Host-Tagging

After much reading of the man pages, and some testing with a pair of virtual machines, I have come up with what I believe is a “smarter” solution to adding hosts to groups.

While most people advocate that classes match on hardware, they are actually capable of matching on quite a few things. For me, the key factor was host-decl-name: this is the name of the host declaration (i.e. “alpha”, “beta” and “gamma” from the example above). There are also a few string manipulation functions available (such as substring, suffix and regex matches) which can be applies to the host-decl-name value.

So, it occurred to me that we can “tag” the host declaration and use that tag to allocate hosts to classes. In my case, I decided to prefix the declarations with “TAG-“. If the tag is “red” or “blue”, then we can adapt the above example to be:

class "red-hosts" {
    match if substring( lcase( host-decl-name ), 0, 3) = "red";
}

class "blue-hosts" {
    match if substring( lcase( host-decl-name ), 0, 4) = "blue";
}

host "red-alpha" {
    hardware ethernet 08:00:2b:4c:39:ad;
}

host "blue-beta" {
    hardware ethernet 08:00:2b:a9:cc:e3;
    fixed-address 192.168.99.99;
}

host "red-gamma" {
    hardware ethernet 00:00:c4:aa:29:44;
}

pool {
    allow members of "red-hosts";
    range 192.168.100.10 192.168.100.200;
}

With this configuration, alpha and gamma join the pool while blue is given his fixed address. In each case, we’ve only had to write the MAC address once.

We can, of course, extend the parsing a little further. DHCP has a use-host-decl-names option which will set the hostname to be equal to the name of the host declaration. Probably we don’t want to expose the tag to the host, so let’s parse the other half of the declaration to get the hostname.

use-host-decl-names off;

class "red-hosts" {
    match if substring( lcase( host-decl-name ), 0, 3) = "red";
}

class "blue-hosts" {
    match if substring( lcase( host-decl-name ), 0, 4) = "blue";
}

if substring( lcase( host-decl-name ), 0, 3) = "red" {
    option host-name = pick-first-value(substring( lcase( host-decl-name ), 4, 256), option host-name, concat( "dhcp-", binary-to-ascii( 16, 8, "-", substring( hardware, 1, 6 ))));
}
if substring( lcase( host-decl-name ), 0, 4) = "blue" {
    option host-name = pick-first-value(substring( lcase( host-decl-name ), 5, 256), option host-name, concat( "dhcp-", binary-to-ascii( 16, 8, "-", substring( hardware, 1, 6 ))));
}

host "red-alpha" {
    hardware ethernet 08:00:2b:4c:39:ad;
}

host "blue-beta" {
    hardware ethernet 08:00:2b:a9:cc:e3;
    fixed-address 192.168.99.99;
}

host "red-gamma" {
    hardware ethernet 00:00:c4:aa:29:44;
}

pool {
    allow members of "red-hosts";
    range 192.168.100.10 192.168.100.200;
}

Things to note here: We use if statements to pick the right length of host-decl-name to skip over (if the tag is red, skip 4 character, if it’s blue, skip 5), we then use pick-first-value to pick the first non-null item from the list of: the right-hand-side of the host-declaration-name (which could be null either if you had a host called ‘red’ or ‘blue’), the host-name provided by the client (not all clients do present a preferred name), a generated name based on the MAC address.

So there we have it, the host name is extracted from the declaration, just like use-host-decl-names used to, but now we can associate hosts to pools/groups in a virtually arbitrary manner.

DynDNS

The last part of the puzzle I wanted to add was Dynamic DNS. Dynamic DNS is a feature whereby the DHCP server talks to the DNS server and says “I just gave a host called ‘alpha.example.org’ an IP address of 192.168.101.20, please update accordingly” and the DNS server will then start telling people that ‘alpha.example.org’ is now at that address.

As I use PowerDNS, and this is an “experimental” feature, I needed to install a third-party build, as noted above. Nicely, there are apt-source lines on that site, so it’s just a matter of adding them to a new file in /etc/apt/sources.list.d, running wget -O- https://ci.namespace.at/debian/signing.key | sudo apt-key add - and then installing the new pdns-server. Note, however, that the version from this repository is lower than that in debian (it’s based on the Git commit id, I believe) so you will probably need to get creative with pinning or holding or whatever.

Once you have PowerDNS updated, it’s mostly a matter of following the excellent PowerDNS HOWTO. Start by creating a TSIG key (dnssec-keygen is in the bind9-utils package):

dnssec-keygen -a hmac-md5 -b 128 -n USER dhcpdupdate

This generates two files (Kdhcpdupdate.*.key and Kdhcpdupdate.*.private). You’re interested in the .key file:

$ ls -l Kdhcp*
-rw------- 1 root root  53 Aug 26 19:29 Kdhcpdupdate.+157+20493.key
-rw------- 1 root root 165 Aug 26 19:29 Kdhcpdupdate.+157+20493.private

$ cat Kdhcpdupdate.+157+20493.key
dhcpdupdate. IN KEY 0 3 157 FYhvwsW1ZtFZqWzsMpqhbg==

The important bits are the name of the key (dhcpdupdate) and the hash of the key (FYhvwsW1ZtFZqWzsMpqhbg==).

Using the details from the key you’ve just generated. Add the following to your dhcpd.conf:

key "dhcpdupdate" {
        algorithm hmac-md5;
        secret "FYhvwsW1ZtFZqWzsMpqhbg==";
};

You must also tell dhcpd that you want dynamic DNS to work, add the following section:

ddns-updates on;
ddns-update-style interim;
update-static-leases on;
deny-client-updates;

This tells dhcpd to:

  • Enable Dynamic DNS

  • Which style it must use (interim) – NOTE: isc-dhcp-server only reads ddns-update-style once and applies it to the whole configuration. It is not possible to mix update styles. In particular, if you have (like I did) ddns-update-style none; at the top of the file, then it won’t get overridden.

  • Update static leases as well

  • Inform clients that they MUST NOT update the DNS themselves.

For more information on this, consult the dhcpd.conf manual.

Per subnet, you also have to tell dhcpd which (reverse-)domain it should update and on which master domain server it is running.

ddns-domainname "example.org";
ddns-hostname = config-option host-name;
ddns-rev-domainname "in-addr.arpa.";

zone example.org {
    primary 127.0.0.1;
    key dhcpdupdate;
}

zone 101.168.192.in-addr.arpa. {
    primary 127.0.0.1;
    key dhcpdupdate;
}

This tells dhcpd a number of things:

  • Which domain to use (ddns-domainname “example.org”;)

  • Which hostname to use (ddns-hostname = config-option host-name). We use the value we will be sending to the host.

  • Which reverse-domain to use (dnssec-rev-domainname “in-addr.arpa.”;)

  • For the zones, where the primary master is located (primary 127.0.0.1;)

  • Which TSIG key to use (key dhcpdupdate;). We defined the key earlier.

A number of small changes are needed to powerdns to make it accept dynamic updates from dhcpd.

Enable RFC2136 (dynamic update) support functionality in PowerDNS by adding the following to the PowerDNS configuration file (pdns.conf).

experimental-rfc2136=yes
allow-2136-from=

This tells PowerDNS to:

  • Enable RFC2136 support(experimental-rfc2136)

  • Allow updates from NO ip-address (allow-2136-from=)

We just told powerdns (via the configuration file) that we accept updates from nobody via the allow-2136-from parameter. That’s not very useful, so we’re going to give permissions per zone, via the domainmetadata table.

sql> select id from domains where name='example.org';
5
sql> insert into domainmetadata(domain_id, kind, content) values(5, 'ALLOW-2136-FROM','127.0.0.1');

This gives the ip ‘127.0.0.1’ access to send update messages. Make sure you use the ip address of the machine that runs dhcpd.

Another thing we want to do, is add TSIG security. This can only be done via the domainmetadata table:

sql> insert into tsigkeys (name, algorithm, secret) values ('dhcpdupdate', 'hmac-md5', 'FYhvwsW1ZtFZqWzsMpqhbg==');
sql> select id from domains where name='example.org';
5
sql> insert into domainmetadata (domain_id, kind, content) values (5, 'TSIG-ALLOW-2136', 'dhcpdupdate');
sql> select id from domains where name='101.168.192.in-addr.arpa';
6
sql> insert into domainmetadata (domain_id, kind, content) values (6, 'TSIG-ALLOW-2136', 'dhcpdupdate');

This will:

  • Add the ‘dhcpdupdate’ key to our PowerDNSinstallation

  • Associate the domains with the given TSIG key

Restart PowerDNS and you should be ready to go!

Bookmark the permalink.

Comments are closed.