Angry robot logo

Mumbling about computers

BSD inspired network setup

Posted on under [ ]

For quite some time, I've been running a dnsmasq instance on my router that was acting as:

  • DHCP server
  • DNS forwarder
  • DNS caching
  • DNS-based ad-blocking
  • Authoritative name server
  • TFTP server (wtf dnsmasq)

After watching EuroBSDCon 2019 I got inspired to try the BSD approach to network services.

The new system looks something like this:

+----------+                     +--------+
|          |                     |        |
| clients  +-------------------->| dhcpd  |
|          |                     |        |
+----+-----+                     +--------+
     |                               |
     |                               v
     |         +------------+    +--------+
     |         |            |    |        |
     +-------->|  unbound   |--->|  nsd   |
               |            |    |        |
               +------------+    +--------+


For most clients, when they connect to the network they will make a DHCP request with dhcpd, and if they provide a name (or are present in the list of MAC-address-to-name mapping), their name and IP address will be added to a local zone in nsd, so that it can be resolved by the other clients. This is particularly handy for things like the set-top box.

Getting client hostnames into the network's DNS was a feature of dnsmasq, but it is not provided by default by the bsd standard utilities, although it is easy to accomplish as both dhcpd and nsd are well suited for a bit of scripting.

dhcpd provides three events: commit, release and expiry. For my usecase, executing a script on commit is enough.

on commit {
    set clhost = pick-first-value(
          option fqdn.hostname,
          option host-name,
    set clip = binary-to-ascii(10, 8, ".", leased-address);
    execute("/root/", clip, clhost);

Which then shows in the logs as

DHCPDISCOVER from 5c:cf:7f:d7:0d:95 (RFPOWER) via lan0
DHCPOFFER on to 5c:cf:7f:d7:0d:95 (RFPOWER) via lan0
data: host_decl_name: not available
execute_statement argv[0] = /root/
execute_statement argv[1] =
execute_statement argv[2] = RFPOWER

The actual script to update nsd is also similarly simple (although it does not handle expiry/release at all), it boils down to:

if [ -z "$found" ]; then
    echo $entry >> $zone_file
    sed -i "s|^$name.*|$entry|" $zone_file

nsd-control reload "$zone_name"


Once clients have an IP address (either static or provided by dhcpd), they will start making requests to resolve domain names to unbound. Most queries will be resolved via the root name servers, and a very small subset of those will be either:

  1. Local client request (for the .labs domain)
  2. Targeted at my public domain (

Those special requests, are handled by a special zone; they are not sent to the root name servers.

The local domain .labs hosts a combination of statically provided domains (for servers) and it also gets updated with dhcpd entries.

For the public domain, I have set up internal A records, as it's way easier to access the services I host always by the same domain. As I also have some subdomains on actual public IPs, I had to also specify those.

de                               IN A
healthchecks                     IN A             IN A
*                                IN A

Blocking Ads

There's another (set of) special zone(s), a big list of 'bad' domains is specified to be marked as rejected forever; this is a neat mechanism to block undesirable domains (mostly ads, but also a good chunk of facebook's and google's domains).

An entry in the blocklist looks like this:

local-zone: "" refuse

It is trivial to wget a bunch of the blocklists that can be found on the internet and adapt them to this format, even so, pay attention and avoid duplicate entries, otherwise unbound will not start.

Privacy concerns

Now that I am no longer forwarding my requests, there's another privacy concern popping up: my IP address and my queries are now exposed to all the nameservers, while before it was only exposed to google/cloudflare/etc.
For the moment, I feel like this is a reasonable tradeoff. We'll see.


Blocking ads 1
Blocking ads 2
Blocking ads 3
NSD Tutorial
Unbound Tutorial
DHCP hostname into DNS
DNS Script idea