It seems that you've found a solution to your problem by adding the Global Domain Route of ~.
to the /etc/systemd/resolved.conf
configuration file. While this appears to work, I feel this approach is limiting in the sense that you're only able to affect changes for global DNS settings within this file and not specific links. But there's a way to get around this roadblock via the D-Bus interface.
D-Bus is an interprocess communication system that allows messages to be sent to various services. In this case, you want to communicate with systemd-resolved to change DNS settings on individual links instead of just globally with the /etc/systemd/resolved.conf
file. There are several ways to communicate with the D-Bus, from a low-level D-Bus API to higher-level bindings. But this can get a bit complicated, so we'll take advantage of a helper script for your openvpn setup to keep things as easy as possible. Within this script, the command, busctl
, is used to communicate with the D-Bus and change settings on individual links. This is more informational than useful at this point, because you won't be interacting with busctl
directly. That's not to say that you can't or won't in the future, though.
Suffice it to say, the Global Domain Route of ~.
is key to controlling the default route of DNS queries. It basically says that DNS queries for all domains not specifically listed on any other link will be routed to the DNS servers associated with the link that the Global Domain Route is set to. That's a mouthful, but hopefully this will make more sense as we proceed.
I'd also like to mention that an advantage of editing individual links instead of defining global DNS settings is that it'll allow more flexibility in the future for situations like Split DNS. I won't go into full detail here, but an explanation of Split DNS as well as routing domains and search domains can be found at these links:
Anyway, I feel that the approach I'm outlining below is probably more in line with the intended use of systemd-resolved than my previous answer. Hence, this is my preferred answer and the way that I would configure my system.
Let's get started...
Setup /etc/resolv.conf
Make sure /etc/resolv.conf
is a symlink to /run/systemd/resolve/stub-resolv.conf
. This is the default setup in Ubuntu 22.04. Even though the local resolver at 127.0.0.53 is used, it still queries any upstream DNS servers for records not in it's cache. It should look like the following:
$ ls -l /etc/resolv.conf
lrwxrwxrwx 1 root root 39 Apr 22 23:47 /etc/resolv.conf -> ../run/systemd/resolve/stub-resolv.conf
If /etc/resolv.conf
is a static file or a symlink to anything other than /run/systemd/resolve/stub-resolv.conf
, do the following:
$ sudo rm /etc/resolv.conf
$ sudo ln -s /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
Setup /etc/systemd/resolved.conf
Next, don't set any global DNS servers in /etc/systemd/resolved.conf
. We'll set specific DNS servers later for your VPN tunnel elsewhere.
Regarding settings for DNS-over-TLS, you can either set them globally or for any specific link. For this setup, we'll set them globally in /etc/systemd/resolved.conf
so that it affects all servers on all links. Additionally, let's set it to opportunistic
instead of yes
so that it'll succeed whether the server supports DNS-over-TLS or not. If you want to enforce DNS-over-TLS and fail if it isn't supported, then change this to yes
.
Edit /etc/systemd/resolved.conf
as follows:
#DNS=
#FallbackDNS=
#Domains=
#DNSSEC=no
DNSOverTLS=opportunistic
#MulticastDNS=no
#LLMNR=no
#Cache=no-negative
#CacheFromLocalhost=no
#DNSStubListener=yes
#DNSStubListenerExtra=
#ReadEtcHosts=yes
#ResolveUnicastSingleLabel=no
Setup openvpn
With openvpn, it's typical that DNS servers are pushed to the client, yet the system isn't automatically configured to use them. This is because the process is slightly different on various operating systems. With Ubuntu, an openvpn installation typically provides a helper script to implement the DNS servers to redirect queries through the tunnel, thus preventing DNS leaks. This is the helper script I mentioned in the beginning that we are going to use.
Here is the current configuration of your .ovpn file:
client
dev tun
proto udp
remote (redacted)
nobind
remote-cert-tls server
tls-version-min 1.2
verify-x509-name (redacted)
cipher AES-256-CBC
auth SHA256
auth-nocache
askpass (redacted)
verb 3
script-security 2
up /etc/openvpn/update-resolv-conf
down /etc/openvpn/update-resolv-conf
redirect-gateway
dhcp-option DNS 10.12.216.2
Look at the following lines from your .ovpn file:
script-security 2
up /etc/openvpn/update-resolv-conf
down /etc/openvpn/update-resolv-conf
This section defines a helper script called, update-resolv-conf
, that runs when the VPN goes up
and down
. Within the script, the application calls /sbin/resolvconf
. But this will fail in the default installation of Ubuntu 22.04, because /sbin/resolvconf
isn't installed. This is because your system is running systemd and uses systemd-resolved.
/sbin/resolvconf
is a framework for managing /etc/resolv.conf
and modifies the config file directly, but this file is no longer used in the same fashion as before. (As you're now aware, it's a symlink to /run/systemd/resolve/stub-resolv.conf
.) Needless to say, you need a different helper script called, update-systemd-resolved
which uses busctl
to access the D-Bus interface.
Install the helper script with the following:
sudo apt install openvpn-systemd-resolved
This places the script in /etc/openvpn
.
After this script is installed, modify your .ovpn file and replace the previous lines with the following:
script-security 2
up /etc/openvpn/update-systemd-resolved
down /etc/openvpn/update-systemd-resolved
down-pre
dhcp-option DOMAIN-ROUTE .
You'll need to reboot the system for this to take effect, but first look at the current output of resolvectl
:
Before any changes to .ovpn:
$ resolvectl
Global
Protocols: -LLMNR -mDNS +DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: stub
DNS Servers:
Link 2 (enp0s10)
Current Scopes: DNS
Protocols: +DefaultRoute +LLMNR -mDNS +DNSOverTLS DNSSEC=no/unsupported
DNS Servers: 192.168.1.1
DNS Domain: localdomain
Link 3 (tun0)
Current Scopes: none
Protocols: -DefaultRoute +LLMNR -mDNS +DNSOverTLS DNSSEC=no/unsupported
Link 4 (wlp5s0)
Current Scopes: none
Protocols: -DefaultRoute +LLMNR -mDNS +DNSOverTLS DNSSEC=no/unsupported
Reboot your system and run resolvectl
again. Take note of the following changes:
- With the addition of
dhcp-option DOMAIN-ROUTE .
to your .ovpn file, the Global Routing Domain ~.
has been added to Link 3 (tun0)
.
- The
-DefaultRoute
Protocol has changed to +DefaultRoute
on Link 3 (tun0)
.
- A DNS server
10.12.216.2
has been added to Link 3 (tun0)
. This is because in your .ovpn file, the line dhcp-option DNS 10.12.216.2
was included. (There might also be additional DNS servers listed for Link 3 (tun0)
that were pushed to you from the VPN server.)
After changes to .ovpn:
$ resolvectl
Global
Protocols: -LLMNR -mDNS +DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: stub
DNS Servers:
Link 2 (enp0s10)
Current Scopes: DNS
Protocols: +DefaultRoute +LLMNR -mDNS +DNSOverTLS DNSSEC=no/unsupported
DNS Servers: 192.168.1.1
DNS Domain: localdomain
Link 3 (tun0)
Current Scopes: none
Protocols: +DefaultRoute +LLMNR -mDNS +DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 10.12.216.2
DNS Servers: 10.12.216.2
DNS Domain: ~.
Link 4 (wlp5s0)
Current Scopes: none
Protocols: -DefaultRoute +LLMNR -mDNS +DNSOverTLS DNSSEC=no/unsupported
At this point, the tunnel will be using 10.12.216.2
as your default DNS server for all domain queries. But if you want to use the DNS servers you originally listed -- and only them --, 9.9.9.9
and 149.112.112.112
, then you need to do the following:
- Ignore any DNS servers pushed to you from your VPN server.
- Specifically list the DNS servers in your .ovpn file that you want to use.
- Omit the current DNS server listed in your .ovpn file
All of this can be done by adding the following to your .ovpn file:
pull-filter ignore "dhcp-option DNS"
dhcp-option DNS 9.9.9.9
dhcp-option DNS 149.112.112.112
And don't forget to delete or comment out dhcp-option DNS 10.12.216.2
that's currently in your .ovpn file.
After the changes, your .ovpn file should look like the following:
client
dev tun
proto udp
remote (redacted)
nobind
remote-cert-tls server
tls-version-min 1.2
verify-x509-name (redacted)
cipher AES-256-CBC
auth SHA256
auth-nocache
askpass (redacted)
verb 3
script-security 2
up /etc/openvpn/update-systemd-resolved
down /etc/openvpn/update-systemd-resolved
down-pre
dhcp-option DOMAIN-ROUTE .
redirect-gateway
pull-filter ignore "dhcp-option DNS"
dhcp-option DNS 9.9.9.9
dhcp-option DNS 149.112.112.112
Reboot and run resolvectl
. Your DNS servers have been added to Link 3 (tun0)
. Any DNS servers previously listed in your .ovpn file or pushed to you from your VPN server are now gone.
$ resolvectl
Global
Protocols: -LLMNR -mDNS +DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: stub
DNS Servers:
Link 2 (enp0s10)
Current Scopes: DNS
Protocols: +DefaultRoute +LLMNR -mDNS +DNSOverTLS DNSSEC=no/unsupported
DNS Servers: 192.168.1.1
DNS Domain: localdomain
Link 3 (tun0)
Current Scopes: none
Protocols: +DefaultRoute +LLMNR -mDNS +DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 9.9.9.9
DNS Servers: 9.9.9.9 149.112.112.112
DNS Domain: ~.
Link 4 (wlp5s0)
Current Scopes: none
Protocols: -DefaultRoute +LLMNR -mDNS +DNSOverTLS DNSSEC=no/unsupported
Conclusion
With your system setup like this, the Global Routing Domain of ~.
that is set on Link 3 (tun0)
states that all DNS queries for domains not specifically listed on a different link will be routed to the DNS servers listed for Link 3 (tun0)
. As an example, if you had ~example.com
set as a routing domain on Link 2 (enp0s10)
, then any DNS queries for domains ending in example.com
would be routed to your local DNS server at 192.168.1.1
. All others would be sent to the DNS servers listed for Link 3 (tun0)
.
As you can see, this is highly customizable. If in the future you want to also configure another VPN (either corporate or private) with specific DNS queries going through that route, then you can do so.
Extra
As a final note, I mentioned earlier that the helper script that was installed makes changes to the system via the D-Bus, specifically with the command busctl
. Feel free to inspect the script and checkout the man page for busctl
here.
An easier interface to use, however, and a command that you're familiar with, is resolvectl
, which is a thin wrapper around the D-Bus interface. Check out the following examples:
Set the Global Routing domain for tun0
:
sudo resolvectl domain tun0 ~.
Clear all domains for tun0
:
sudo resolvectl domain tun0 ""
Set tun0
as a default route:
sudo resolvectl default-route tun0 yes
Define DNS server for tun0
:
sudo resolvectl dns tun0 9.9.9.9
Clear DNS Servers for tun0
:
sudo resolvectl dns tun0 ""
Set DNS-over-TLS for tun0
:
sudo resolvectl dnsovertls tun0 yes
With these commands and many more not shown, you can change settings for individual links. And what's cool is that the change is immediate. There's no need to reboot or restart a service, so it's a fun way to play and experiment. Unfortunately, these changes won't survive a reboot, so that's why the helper script used above is beneficial. Without it, you'd have to write your own.
To learn more about changing DNS settings per-link using resolvectl
, check out the man page here. Meanwhile, here's the relevant section:
dns [LINK [SERVER...]], domain [LINK [DOMAIN...]], default-route
[LINK [BOOL...]], llmnr [LINK [MODE]], mdns [LINK [MODE]], dnssec
[LINK [MODE]], dnsovertls [LINK [MODE]], nta [LINK [DOMAIN...]]
Get/set per-interface DNS configuration. These commands may
be used to configure various DNS settings for network
interfaces. These commands may be used to inform
systemd-resolved or systemd-networkd about per-interface DNS
configuration determined through external means. The dns
command expects IPv4 or IPv6 address specifications of DNS
servers to use. Each address can optionally take a port
number separated with ":", a network interface name or index
separated with "%", and a Server Name Indication (SNI)
separated with "#". When IPv6 address is specified with a
port number, then the address must be in the square brackets.
That is, the acceptable full formats are
"111.222.333.444:9953%ifname#example.com" for IPv4 and
"[1111:2222::3333]:9953%ifname#example.com" for IPv6. The
domain command expects valid DNS domains, possibly prefixed
with "~", and configures a per-interface search or route-only
domain. The default-route command expects a boolean
parameter, and configures whether the link may be used as
default route for DNS lookups, i.e. if it is suitable for
lookups on domains no other link explicitly is configured
for. The llmnr, mdns, dnssec and dnsovertls commands may be
used to configure the per-interface LLMNR, MulticastDNS,
DNSSEC and DNSOverTLS settings. Finally, nta command may be
used to configure additional per-interface DNSSEC NTA
domains.
Commands dns, domain and nta can take a single empty string
argument to clear their respective value lists.
For details about these settings, their possible values and
their effect, see the corresponding settings in
systemd.network(5).