Split DNS with systemd
I've had to set up VPNs for organizations in the past. Every time I'd set one up with an intranet domain, all DNS requests from users would be forwarded to the organization's DNS server. This worked and was simple but I didn't like it, because I'd have to manage more private data from users than I needed. What if we could avoid forwarding regular requests to the organization's DNS server? This is one of the applications of what is often called split DNS, and I'll show you an example here.
Note that what I'm about to describe doesn't apply to VPN you'd use for all your network traffic. In such a case, forwarding all DNS requests via the VPN is actually important.
Example with a corporate VPN
Let's say the internal domain is intranet.example.com
where users have access services
such as gitlab.intranet.example.com
. There are different ways to achieve this. In
general, if you have many users and many services, setting up a DNS server for the
organization is the way to go. Unfortunately, after users have configured their system to
use that DNS server, that server receives all DNS requests, not just those for
gitlab.intranet.example.com
. At least that's my experience with Windows or Linux.
Note that it is often possible to add what is called a "secondary DNS server". That server is used when the primary server is unavailable regardless of the domain of DNS queries. This feature is thus useless for split DNS.
Solution for Linux with systemd
systemd, now the most common init system, comes with systemd-resolved. Instead of editing
/etc/resolv.conf
, you can configure the resolver with resolvectl
.
For example, with the main interface named enp0s3
and the VPN interface named tun0
,
you can use the following command:
resolvectl domain tun0 ~intranet.example.com
Afterwards, only requests for intranet.example.com
and its subdomains are sent to the
DNS server via the VPN. Other requests are forwarded to the DNS server available via your
LAN.
You can check the configuration with the command resolvectl
:
Global
[...]
Link 2 (enp0s3)
Current Scopes: DNS LLMNR/IPv4 LLMNR/IPv6
Protocols: +DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 192.0.2.1
DNS Servers: 192.0.2.1 2001:db8:aaaa:3
Link 3 (tun0)
Current Scopes: DNS
Protocols: -DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 2001:db8:bbbb:3
DNS Servers: 2001:db8:bbbb:3
DNS Domain: ~internal
If you want to be sure that it is correctly configured, you can ask resolvectl
to do
some queries. For example:
> resolvectl query example.com example.com: 2606:2800:220:1:248:1893:25c8:1946 -- link: enp0s3 93.184.216.34 -- link: enp0s3 -- Information acquired via protocol DNS in 5.3ms. -- Data is authenticated: no; Data was acquired via local or encrypted transport: no -- Data from: network
We can see here that it used the local Ethernet interface, enp0s3
, not tun0
.
To be extra sure that it worked as expected, I monitored outgoing DNS requests with
Wireshark and observed the result of running ping foo.example.com
or ping
foo.intranet.example.com
.
Use with WireGuard and OpenVPN
With WireGuard, if you use wg-quick
:
[Interface] PrivateKey = ... Address = 2001:db8:bbbb:1 DNS = 2001:db8:bbbb:3 PostUp = resolvectl domain tun0 ~intranet.example.com [Peer] ...
I haven't tested split DNS with OpenVPN but it should be possible to have it run the
desired command with the up
option in the client configuration:
script-security 2
up resolvectl domain tun0 ~intranet.example.com
See this StackExchange question for more information.
Conclusion
The new systemd component, systemd-resolved, gives you the power to do split DNS over different network interfaces. This is useful because it reduces the amount of private data sent to the DNS server available via the VPN.
If you don't have systemd or need more control over your DNS requests, I would probably recommend using a local resolver such as Unbound or dnsmasq.