How Linux Manages Split DNS with systemd-resolved

Split DNS allows a Linux system to route DNS queries to different nameservers based on the requested domain name, which is vital when connected to private networks or VPNs while maintaining standard internet access. In modern Linux distributions, this behavior is handled natively by systemd-resolved. This article explains how systemd-resolved tracks per-link network configurations, utilizes routing domain prefixes, evaluates query scopes, and directs domain requests to the appropriate nameservers seamlessly.

Understanding the Split DNS Problem

Traditional Linux DNS resolution relies on /etc/resolv.conf, which applies nameservers globally. In this legacy model, the resolver queries the listed servers sequentially. If a corporate VPN adds its DNS server to the top of the list, that server receives all DNS traffic—including non-work requests—raising privacy and performance issues. Conversely, if it is placed at the bottom, private hostnames fail to resolve if the primary public server returns an NXDOMAIN (non-existent domain) error.

systemd-resolved solves this by introducing per-interface DNS awareness directly into the network stack.

Per-Interface DNS Configuration

Instead of maintaining a single global set of DNS servers, systemd-resolved associates DNS servers and domain configurations with specific network interfaces (such as eth0, wlan0, or a VPN interface like tun0 or wg0).

Each interface can define two types of domains:

  1. Search Domains: Suffixes appended to single-label queries (e.g., querying intranet becomes intranet.company.internal).
  2. Routing Domains: Domains used exclusively to direct queries to a specific interface's DNS server, without automatically appending them to search lists.

The Routing Domain Syntax (~)

The core mechanism for split DNS in systemd-resolved is the routing domain syntax. When defining domains for a specific link, prefixing a domain name with a tilde (~) designates it strictly as a routing domain.

Query Routing Logic

When an application makes a DNS query via the local stub listener (127.0.0.53), systemd-resolved evaluates the destination domain against all known link configurations using the following rules:

  1. Most Specific Match Wins: If a query matches a defined routing or search domain, it is routed to the interface with the most specific match. For example, a query for service.dev.corp.example.com will prefer an interface with ~corp.example.com over an interface assigned the default catch-all ~..
  2. Parallel Queries (Fallback): If multiple interfaces match with equal specificity, systemd-resolved sends queries to all matching interfaces in parallel, returning the first valid answer received.
  3. Loopback Scope: Single-label hostnames or local names matching mDNS or LLMNR configurations bypass interface-specific routing rules and are handled by their respective protocols if enabled.

Managing Split DNS with resolvectl

System administrators and VPN clients (like OpenVPN, WireGuard, or NetworkManager) control this behavior dynamically via the resolvectl command-line utility.

To configure split DNS manually for an interface named tun0:

  1. Assign nameservers to the interface:

    resolvectl dns tun0 10.0.0.2 10.0.0.3
  2. Assign routing domains to the interface:

    resolvectl domain tun0 ~company.internal ~corp.example.com
  3. Verify the routing configuration:

    resolvectl status tun0

This configuration ensures that any request targeting *.company.internal or *.corp.example.com is routed through 10.0.0.2 or 10.0.0.3 over tun0, while all other internet requests continue through the default gateway's DNS servers.

Integration with NetworkManager and VPN Clients

Most modern network managers integrate automatically with systemd-resolved. When a VPN connection establishes:

Through this per-link architecture and domain-matching logic, systemd-resolved delivers deterministic, secure, and automated split DNS routing across complex multi-homed Linux environments.