Intelligent DNS Resolution with systemd

The Problem

Historically, local DNS resolvers were pretty stupid, and UNIX was no exception. You'd stuff some nameservers and DNS suffixes into /etc/resolv.conf and you were more-or-less done. Want to send some queries to one nameserver and others to another? You would have to run your own local nameserver and configure those policies there - and if you wanted do do that differently based on what network you were on or if you brought up a VPN tunnel, you'd have to script all that as well. It wasn't impossible, but it wasn't easy either. I even did all that sometimes, but every time I reinstalled I'd have to go through it all over again, and that just sucked.

Enter systemd

When systemd first came out, I was firmly in the camp of people that did not like it. I was perfectly happy with the old school init systems used by the various UNIX / Linux flavors and didn't see much of a reason to change. I'll admit though, even though I'm far from a systemd wizard, the more I use it and the more it worms its way into the guts of Linux, the more I sorta like it.

I still find myself hating on NetworkManager - but systemd-networkd and systemd-resolved solve real problems it would seem - including the lack of configurability of the system resolver. Between those two daemons, you can configure your system with some policies for how to bring up the network and name resolution when various things happen. The documentation, while voluminous, doesn't actually tell you what to do - so I'm going to do that here.

The Solution

At the time of writing this, I'm running Ubuntu 19.10 on the laptop (a Dell Precision 5540) provided by my employeer, and I managed to get it to handle using the local nameservers at my home when I'm there, at work when I'm there, to use a generic fallback on the internet when I'm on the road, and to use a remote nameserver for only some zones when I'm signed into a VPN - and it automatically figures it out without me needing to do anything.

First, in my laptop's BIOS I had to disable a feature to use the system's well-known MAC address when plugged into a docking station. I have docks at my desks at home and work, and I want them to have different MACs so that I can key the matching off them. It's called MAC Address Pass-Through in my BIOS. You probably won't need to do this as I had never seen the feature before - but maybe you will!

Next, you need to enable the systemd-networkd daemon and start it up. That's sudo systemctl enable systemd-networkd and sudo systemctl start systemd-networkd.

Check out /etc/resolv.conf -l see if it's a normal file or not: ls -l /etc/resolv.conf. I recommend making it a symlink to the stub resolver provided by systemd-resolved. So delete what's there: sudo rm -f /etc/resolv.conf and replace it with: sudo ln -s /run/systemd/resolve/stub-resolv.conf. The stub resolver is maintained by systemd and will cause the system resolver provided by gethostbyname() in libc to fall through to systemd's stub resolver which implements all the policies we're going to lay out.

In order to use a sane fallback IP when on the road, change /etc/systemd/resolved.conf so that it looks something like:

[Resolve]
FallbackDNS=9.9.9.9

That'll set you up so that if you're lacking any other policies, you'll send queries to whatever nameserver you configure here. I recommend making it one of the big providers that hopefully won't spy on you. There are also some options in this file you can set to enable encrypted DNS and such - check out the docs if interested.

Now to implement the policies for how to configure things in different environments. You do that by dropping files ending in ".network" into /etc/systemd/network/ - I have one for work, one for home, and another for a VPN that I use to access a remote lab.

For home, I have something like:

[Match]
MACAddress=<my mac obtained from 'ip link'>

[Network]
DHCP=ipv4
DNS=<first DNS server to use>
DNS=<second DNS server to use>
Domains=<my home prefix> ~.
DNSDefaultRoute=true

[DHCP]
UseDNS=true

You can probably leave out the DNS servers and it'll get them from DHCP - but no harm in listing them as long as you don't expect them to change. One subtle point here is the presence of ~. as a domain - this along with DNSDefaultRoute=true tells systemd-resolved to use this link's nameservers to resolve queries that don't match any other configurations. This effectively masks the fallback we configured above which is probably what you want so that you can benefit from local DNS caches to make things a bit snappier.

For work, I have a similar file, but with a different MAC, nameservers, and domain. It also specifies ~. and DNSDefaultRoute=true.

Then, I have one for my lab VPN. I purposefully don't want to accept what it provides, so I use this to suppress it.

[Match]
Name=tun0

[Network]
DHCP=ipv4
DNS=<lab DNS server>
Domains=~<lap zones>

[DHCP]
UseDNS=true
CriticalConnection=yes

Important things here... make sure you match the right tunnel name. If you use multiple VPNs, this might get messy... sorry! Also, if needed supply the DNS server you want to use, as well as any DNS suffixes to send to it prepended with a tilde (~). This tells systemd-resolved to send queries that match these zones to the supplied nameservers. That's tight!

The End

All that said, I still have to fight with this sometimes. I left wireless out of this entirely - and I haven't tried to setup bridges to get VMs online - or to have those bridges' uplinks change based on wifi vs ethernet. That said, I'm pretty happy with what I've got so far.