Home » Linux » How To Craft Advanced DNS Configurations on Linux

How To Craft Advanced DNS Configurations on Linux

network engineer

As readers of my previous desktop Linux DNS article will be able to attest, systemd’s management of DNS is complex. By putting time into comprehending its complexity, though, we can create nuanced DNS resolution behaviors for specialized use cases.

To pick back up, this installment will start by fleshing out how systemd-resolved routes queries. From there, I will outline how to configure DNS on a per-link basis. To close, I will reflect on why it’s so difficult to get simple, consistent, and actionable information on this subject.

Everything in Its Proper Domain

Last time, I made cursory mention that, in systemd-resolved’s configuration, “~.” is a special route-only domain that specifies the “default” link (or global) to use for DNS resolution. Let’s go into detail.

In systemd-resolved’s split DNS, there are two modes of specifying which queries go to which links/global: search domains and route-only domains. Both search and route-only domains are set to a domain string as a value (e.g., “linuxinsider.com”). Each link/global can only have one domain configured, either a route-only or search domain (not both).

When a domain is submitted for a query, it is compared against all of the search and route-only domains that have configured (i.e., non-empty) values.

If (only) one of the configured search/route-only domain values is found within the domain being queried, this is considered a “match,” and the query is sent to the link with the matching search/route-only domain.

If multiple search/route-only domains match, the query is sent to the link/global whose search/route-only domain is the longest.

If none of the search/route-only domains match, all links and global get sent the DNS query. Since the whole point of DNS is that it is a distributed but consistent system, they should all give you the same answer, but it still sinks unnecessary time and compute resources into the query.

Search and route-only domains are almost identical in functionality. In my research, I didn’t get as clear an answer as I’d hoped on the difference. As the Fedora Magazine article and Gnome Foundation blog post indicate, and the systemd-resolved man page corroborates, the main distinction is in handling “single-label” domains.

With search domains, if you are querying a “single-label” domain with no “.” characters in it, the query is executed with the search domain’s value appended to it. For instance, if we have a search domain for “example.com” configured on a link, and we query “mail,” the submitted query would go to that link and be queried as “mail.example.com.” By implication, we can say that single-label queries are always tried against all configured search domains, and by definition, single-label queries are not going to match any route-only or search domains on their own because they have at least one “.” in them.

The other notable distinction is how search and route-only domains are configured. All configured per-link domains are search domains by default. Prepending a “~” to the domain makes it a route-only domain.

This brings us to our friend “~.”. As alluded to in the preceding piece, “~.” matches any query that doesn’t match any other route-only or search domains. Thinking about it, that makes sense. It is functionally the default because all domains (except single-label) will always match since they all have “.” in them. But this domain match will always be shorter than any other possible route-only/search domain. Hence, it’s a default.

The querying order and return conditions are even more complex than this. If you’re dying to know everything, crack open the “systemd-resolved” man page linked above. But this is really all we need to understand systemd-resolved to a practical degree.

Reassembling All the Split Pieces

Now we’re ready to understand systemd-resolved’s split DNS behavior.

To adapt an example from the Gnome Foundation piece, let’s take the common scenario of connecting to a work VPN. In Linux, VPNs (among other networking configurations) can create a virtual networking interface. Your computer treats it like any other networking interface (e.g., a wireless card), but it is virtual (i.e., software rather than hardware). Further, systemd-resolved also treats this virtual networking interface as a link. Thus, it can have its own DNS configuration.

On a work VPN, you might need to query work-related domains against a DNS server on the network across the VPN tunnel. Well-implemented Linux VPN software will be designed to make the correct systemd-resolved API calls to configure its link in systemd-resolved with any route-only or search domain, as well as any DNS servers it needs.

As such, let’s say the VPN link has “ecorp.com” as its search domain, and let’s say this is in addition to your wireless card that has the “~.” route-only domain. The result is that any domains you query that contain “ecorp.com” will go to the DNS servers set on the VPN link. All other DNS queries would go to the wireless link’s servers.

So, what happens if we leave “~.” out of our wireless link? Domain queries containing “ecorp.com” or single-label queries will function the same as before. However, any domains not matching the VPN link’s search domain will be queried on all links/global. If you don’t want the corporate VPN to see where you’re browsing, this is undesirable.

The Missing Link Files

Although we determined in the preceding piece in this series that if all you want is to override the DNS servers provided by your access point (AP), changing systemd-resolved’s global configuration is best. However, you may encounter situations where you need to tweak the DNS behavior on a specific link.

To do that, we turn to systemd-networkd, another daemon that controls networking behavior. systemd-networkd handles all your network devices according to its own defaults, but you can give it custom instructions. If systemd-networkd finds files in the /etc/systemd/network directory with the correct filename conventions and file contents, it will do as you tell it.

Besides your desired DNS servers, all you need to know going in is the name of your network interface. To view your interfaces, run ip link. For most desktop Linux installations, the only interfaces you’ll likely find are “lo” (“loopback,” a self-reference), a wireless interface, and maybe an Ethernet interface.

Interface name in hand, create a file for your interface (as superuser) in /etc/systemd/network with a filename ending in “.network”. Set the contents to the below, with some minor adjustments.





Replace “interface” with the name you got from ip link, and “server1” and “server2” with whatever DNS server IP addresses you want. You must declare one server, but you can have three at most.

systemd network file screenshot

There are a couple of items I want to shed light on.

One, enabling “DHCP” is very important. DHCP is enabled by default if you don’t have a “.network” file for an interface. But once you create this file, it’s disabled unless you enable it. This is important because an AP uses DHCP to give your computer an IP address on its network. Without it, your computer is functionally invisible.

Two, disabling “UseDNS” is equally important. This is the toggle that determines whether your computer accepts the DNS servers the AP gives it (“yes”) or not (“no”). Since our objective is to customize DNS, we need to tell the AP, “No thanks, I brought my own servers.”

The “DNS” and “Domains” items work as in systemd-resolved.

Once you save the file, reload systemd-networkd and systemd-resolved to reconfigure them. On my Linux Mint system, I noticed that systemd-networkd is not on by default. You can check if it’s enabled by running systemctl status systemd-networkd. If you find it is disabled, run these commands with superuser privileges.

systemctl enable systemd-networkd
systemctl start systemd-networkd
systemctl restart systemd-resolved

Otherwise, run these (also as superuser).

systemctl restart systemd-networkd
systemctl restart systemd-resolved

The process for verifying your settings is the same as changing systemd-resolved’s global settings, so follow those steps in my previous article.

Querying for Answers in the Wrong Places

My biggest motivation for writing these articles, more than teaching DNS configuration, was to illustrate the importance of vetting information and doing things the right way. There’s a lot of bad advice on the topic of Linux DNS customization. Here are some examples I encountered.

Overwrite /etc/resolv.conf and then make the file immutable using Linux file permissions. Brute force methods like this are never wise. Because systemd-resolved is still running, you are making your system waste resources futilely writing to an immutable file. This might not work, either, because many Linux distros pass queries to systemd-resolved via the D-Bus, not by querying the stub listener raw via /etc/resolv.conf.

Disable systemd-resolved to make /etc/resolv.conf a static file again, then write your changes into it. As much as I’d love to revive the old ways, systemd is too embedded to go back. Moreover, important system components rely on systemd-resolved, so if you turn it off, they might break.

Install dnsmasq for DNS caching. This is unnecessary because systemd-resolved already caches DNS responses. Why take up the disk space and duplicate the resources? I can understand an ordinary Linux user not knowing this, but can one dispense this advice without knowing it’s redundant?

Closing Address

With this exercise behind me, I took away two lessons worth underscoring.

First, if you seek answers online, check them against the manual. Even when studying the sources I cited, which were assessed to be credible, I still checked them against the manual. The man pages are ground truth and come preinstalled, so why wouldn’t you consult them?

Second, the only way to truly validate your work is to read the logs. I have my gripes with systemd, but one thing it does well is logging. Because different systemd components are executed as different system users, you can run journalctl and specify the user whose logs you want.

Hopefully, I’ve done my job, and I’m not the only one who learned something from this.

Suggest a Topic

Is there a tutorial you’d like to see featured?

Email your ideas to me, and I’ll consider them for a future column.

And use the Reader Comments feature below to provide your input!