How to Configure systemd-resolved for Custom DNS Routing on Ubuntu

The Evolution of DNS Resolution in Ubuntu

In legacy Linux distributions, DNS resolution was primarily handled by the /etc/resolv.conf file, which was often directly modified by network managers or DHCP clients. However, modern Ubuntu systems (and many other systemd-based distributions) utilize systemd-resolved as a local DNS stub resolver. This service provides a centralized, highly efficient way to manage DNS queries, caching, and DNSSEC validation.

One of the most powerful but underutilized features of systemd-resolved is its ability to perform split DNS routing. This means you can instruct the system to send queries for a specific corporate domain (e.g., company.internal) to a specific internal DNS server over a VPN, while sending all other general internet queries to a public resolver like Google (8.8.8.8) or Cloudflare (1.1.1.1).

This guide explains how to configure custom DNS routing using systemd-resolved and Netplan on Ubuntu.

Step 1: Understanding the systemd-resolved Architecture

Before modifying configurations, it is crucial to understand how systemd-resolved operates. The service listens on a local loopback address (127.0.0.53:53) and intercepts all DNS requests made by local applications. It then forwards these requests to the appropriate upstream DNS servers based on its routing table.

You can view the current state, configured DNS servers, and routing domains by running:

resolvectl status

The output will show a block for each network interface. If an interface has a specific “DNS Domain” attached to it, queries for that domain will be routed exclusively out of that interface.

Step 2: Configuring Split DNS with Netplan

On modern Ubuntu servers, network configuration is managed by Netplan. To configure split DNS routing, we need to modify the Netplan YAML configuration file.

Open your Netplan configuration file (typically located in /etc/netplan/):

sudo nano /etc/netplan/01-netcfg.yaml

Assume you have an interface named eth0 connected to the internet, and a VPN interface named tun0 connected to your corporate network. We want queries for company.internal to go to 10.0.0.5 on the VPN, and everything else to go to 8.8.8.8.

Modify the YAML file to look like this:

network:
  version: 2
  ethernets:
    eth0:
      dhcp4: no
      addresses:
        - 192.168.1.100/24
      routes:
        - to: default
          via: 192.168.1.1
      nameservers:
        addresses:
          - 8.8.8.8
          - 1.1.1.1
        # The "~" denotes a routing domain. "~." means route all default traffic here.
        search: [ "~." ]

  tunnels:
    tun0:
      mode: tun
      # (VPN tunnel configuration omitted for brevity)
      nameservers:
        addresses:
          - 10.0.0.5
        # Route ONLY queries for company.internal to this interface
        search: [ "~company.internal" ]

Step 3: The Importance of the Tilde (~) Prefix

In Netplan’s search configuration (which maps directly to systemd-resolved’s Domains property), the presence of a tilde ~ changes the behavior of the domain.

  • company.internal (without tilde): This is a standard search domain. If you ping “server1”, the system appends “company.internal” and resolves “server1.company.internal”.
  • ~company.internal (with tilde): This is a routing domain. It instructs systemd-resolved to strictly route any query ending in “.company.internal” to the DNS servers associated with this specific interface.
  • ~. (tilde dot): This is the global routing domain. It serves as the default route for all queries that do not match a more specific routing domain.

Step 4: Applying and Verifying the Configuration

Save the Netplan file and apply the configuration:

sudo netplan apply

To verify that systemd-resolved has correctly assimilated the routing logic, run the resolvectl status command again. You should see ~. under the Domains section for eth0, and ~company.internal under the Domains section for tun0.

To test the routing in real-time, you can use the resolvectl query command and monitor the log output to see which interface systemd-resolved chooses to use:

resolvectl query db.company.internal

Step 5: Bypassing systemd-resolved for Specific Applications

Occasionally, you may run into an application (like a custom database client or a legacy Java app) that bypasses the local 127.0.0.53 stub resolver and attempts to read /etc/resolv.conf directly, bypassing your careful routing logic.

To ensure total compliance, ensure that /etc/resolv.conf is a symbolic link to the stub resolver provided by systemd:

sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf

Conclusion

Mastering systemd-resolved and Netplan is essential for modern Ubuntu administration. By leveraging routing domains, administrators can easily build robust split-DNS architectures, ensuring that corporate traffic flows securely over private tunnels while maintaining high-speed public resolution for standard internet traffic.

Get the best tech tips delivered straight to your inbox.

Join thousands of readers mastering Apple, Google, Microsoft, and Linux.