How to Configure systemd-resolved, DNS Caching, and Split-DNS VPN Routing in Ubuntu

Modern Linux networking relies on fast, deterministic domain name resolution to connect distributed microservices, resolve package repositories, and authenticate against corporate directory services. In Ubuntu 24.04 and 22.04 LTS, name resolution is governed by systemd-resolved, a local caching and network-aware DNS stub resolver. Historically, Linux administrators managed nameservers through static entries in /etc/resolv.conf. However, in modern environments where dynamic DHCP interfaces, container bridges, and WireGuard or OpenVPN tunnels coexist simultaneously, static configuration breaks. In particular, remote enterprise workers require “Split-DNS”—routing internal corporate queries (e.g., *.corp.internal) through private VPN nameservers while directing public internet queries through fast local resolvers. Mastering systemd-resolved and the resolvectl command is essential for calibrating DNS caching and establishing leak-free split-DNS routing in Ubuntu.

Understanding systemd-resolved Architecture and the 127.0.0.53 Stub

In Ubuntu, /etc/resolv.conf is not a static text file; it is a symlink pointing to /run/systemd/resolve/stub-resolv.conf. Inside, the nameserver is declared as 127.0.0.53. The table below delineates the functional layers of this architecture:

Resolution Layer Network Address Operational Role Administrative Inspection Tool
Local Stub Resolver 127.0.0.53:53 Listens locally on loopback; intercepts POSIX standard glibc queries. resolvectl status
In-Memory Cache Kernel memory Caches positive and negative DNS responses to accelerate query times. resolvectl statistics
Link-Specific DNS Interface bound (e.g., eth0, wg0) Assigns dedicated nameservers to specific physical or virtual network adapters. resolvectl dns <interface>
Routing Domains (~domain) Routing filter rules Routes specific domain queries strictly through designated interfaces. resolvectl domain <interface>

Querying and Auditing DNS State with resolvectl

The primary command-line tool for interacting with systemd-resolved is resolvectl (which supersedes legacy systemd-resolve).

1. Inspecting Global and Link-Specific Resolvers

resolvectl status

The console output summarizes global fallback servers followed by per-interface telemetry:

Link 2 (eth0):
    Current Scopes: DNS
         Protocols: +DefaultRoute +LLMNR -mDNS +DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 192.168.1.1
       DNS Servers: 192.168.1.1 1.1.1.1
        DNS Domain: home.arpa

2. Testing Live Domain Lookups

To inspect the exact routing path, latency, and returned IP addresses for a specific hostname:

resolvectl query api.ubuntu.com

The output reports the resolved A and AAAA records, along with the specific network link and upstream nameserver that answered the request.

Configuring DNS Caching and Flush Management

systemd-resolved automatically maintains an internal cache of DNS resource records, honouring Time-To-Live (TTL) values while capping stale records.

1. Auditing Cache Hit Ratios

resolvectl statistics

Review the performance telemetry:

  • Transactions: Total number of DNS queries evaluated.
  • Cache Hits / Cache Misses: The efficiency percentage of your local resolver cache.
  • DNSSEC: Secure verification pass/fail tallies.

2. Flushing the Local DNS Cache

When domain DNS records are updated or a server moves to a new IP address, purge stale records from memory:

sudo resolvectl flush-caches

Verify that cache entries reset to zero using resolvectl statistics.

Configuring Split-DNS Routing for Enterprise VPNs

When connecting to an enterprise VPN (e.g., WireGuard on wg0 or OpenVPN on tun0), standard configurations frequently encounter two major flaws: either all internet traffic is routed through a congested corporate gateway, or internal corporate hostnames fail to resolve because public nameservers are queried first. Split-DNS resolves internal hostnames over the VPN while routing public lookups over your local network.

1. Understanding the Tilde (~) Routing Domain Operator

In systemd-resolved, prefixing a search domain with a tilde (~) declares it as a Routing Domain rather than a standard search domain. When a domain is declared with a tilde (e.g., ~corp.internal), systemd-resolved routes any query ending in corp.internal exclusively to the nameservers assigned to that specific interface.

2. Applying Link-Specific Nameservers and Routing Domains

Assume your WireGuard VPN interface is named wg0 and your corporate DNS server is 10.50.0.2:

# Step 1: Assign the corporate DNS server strictly to the VPN interface
sudo resolvectl dns wg0 10.50.0.2 10.50.0.3

# Step 2: Bind the private corporate domains to the VPN interface using the tilde prefix
sudo resolvectl domain wg0 "~corp.internal" "~dev.internal"

# Step 3: Ensure the VPN interface does not claim default route for all public DNS
sudo resolvectl default-route wg0 no

3. Verifying Split-DNS Isolation

Test resolution across both namespaces:

  • Query an internal database: resolvectl query postgres.corp.internal. The output proves the request was routed strictly across Link wg0 to 10.50.0.2.
  • Query a public site: resolvectl query github.com. The output proves the request was routed across Link eth0 to your local high-speed gateway, bypassing the VPN tunnel entirely.

Configuring Global Defaults in /etc/systemd/resolved.conf

To establish persistent upstream resolvers and enable cryptographic DNS security (DNS-over-TLS), edit the primary configuration daemon file:

sudo nano /etc/systemd/resolved.conf

Populate the [Resolve] block with modern security parameters:

[Resolve]
DNS=1.1.1.1#cloudflare-dns.com 8.8.8.8#dns.google
FallbackDNS=9.9.9.9 149.112.112.112
Domains=~.
DNSSEC=allow-downgrade
DNSOverTLS=opportunistic
Cache=yes
CacheTimeSlice=300
  • DNSOverTLS=opportunistic: Encrypts DNS queries using TLS if the upstream provider supports it, falling back to standard plaintext port 53 if TLS handshakes fail.
  • DNSSEC=allow-downgrade: Validates cryptographic DNSSEC signatures on supporting domains without breaking legacy un-signed domains.
  • Domains=~.: Treats the configured global DNS servers as the preferred default route for unmatched public queries.

Restart the daemon to apply your changes:

sudo systemctl restart systemd-resolved

Troubleshooting Common systemd-resolved Faults

Resolve operational DNS stalls using these diagnostic checks:

  • Broken /etc/resolv.conf Symlink: If third-party utilities overwrite /etc/resolv.conf with static files, restore the native systemd symlink:
    sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
  • Port 53 Binding Conflicts: If you are deploying local DNS servers like BIND9, Dnsmasq, or Pi-hole, they will conflict with systemd-resolved‘s stub on port 53. Disable the local stub listener by setting DNSStubListener=no inside /etc/systemd/resolved.conf and updating /etc/resolv.conf to point directly to /run/systemd/resolve/resolv.conf.

Get the best tech tips delivered straight to your inbox.

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