Systemd-resolved DNS configuration for VPN

| 🤔 | 👍 | 👎 |

Some guidance about configuring/fixing domain name resolution with a corporate VPN (especially OpenVPN) with systemd-based Linux systems. This configuration uses the internal/private corporate resolvers for resolving internal/private domain names while using the ISP resolver for general domain names. This might help if your VPN is struggling these days because of the COVID-19 threat 😷.

I've been helping some people to make corporate VPNs (OpenVPN-based) work correctly, since yesterday. The problem is the resolution of domain names:

  1. either private internal domain names are not resolved because the internal corporate resolver is used;

  2. or general domain name resolution is slowish because the internal corporate resolver is used and the VPN is currently struggling with the new load.

A good solution is to use the internal coporate resolver for internal domain names but another (eg. the ISP) resolver for other domain names. Depending on your configuration and how you are running the VPN (CLI, NetworkManager, etc.), this might be already what is happening (you might find some information in this post in order to check this).

Below are some instructions about one solution to make this work on systemd-based Linux systems using systemd-resolved. There are solution to achieve this without systemd-resolved (I think it's possible to do that with dnsmasq).

Warning: don't break your internet access right now if you don't think you are going to be able to fix it.

Manual configuration

Summary

If you are using a Linux system with systemd-resolved, a quick fix is something like this (your need to adjust these values):

# Configure internal corporate domain name resolvers:
resolvectl dns tun0 192.0.2.53 192.0.2.54

# Only use the internal corporate resolvers for domain names under these:
resolvectl domain tun0 "~foo.example.com" "~bar.example.com"

# Not super nice, but might be needed:
resolvectl dnssec tun0 off

If you don't have the resolvectl commands, you might use instead:

systemd-resolve -i tun0 \
  --set-dns=192.0.2.53 --set-dns=192.0.2.54 \
  --set-domain=foo.example.com --set-domain=bar.example.com \
  --set-dnssec=off  # <- Not super nice, but might be needed.

Prerequisites

First check if you have systemd-resolved installed and running:

systemctl status systemd-resolved

Check if you have the resolvectl too:

resolvectl

An alternative is to use systemd-resolve:

systemd-resolve --status

Check if have the resolve mechanism enabled in nssswitch.conf:

cat /etc/nsswitch.conf  | grep ^hosts: | grep resolve

Install the needed libraries. For example on Debian or Ubuntu AMD64 systems:

dpkg -l | grep libnss-resolve

Note: I'm installing both the 64 bit and 32 bit libraries. If you do not install the 32 bit libraries, proper DNS resolution might fail for 32 programs.

Configure DNS resolution

If all these prerequisites are satisfied, you can configure the DNS resolution with something like:

resolvectl dns tun0 192.0.2.53 192.0.2.54
resolvectl domain tun0 "~foo.example.com" "~bar.example.com"

Alternatively, if you don't have resolvectl:

systemd-resolve -i tun0 \
  --set-dns=192.0.2.53 --set-dns=192.0.2.54 \
  --set-domain=~foo.example.com --set-domain=~bar.example.com

This configures systemd-resolved to use the corporate internal resolvers 192.0.2.53 and 192.0.2.54 for resolving all domain names under foo.example.com and bar.example.com (eg. some-service.foo.example.com). tun0 is network interface of the VPN. You have to adjust these parameters (IP address, domain names).

Note: the ~ in front of a domain prevents the domain to be added to the search list (i.e. trying to resolve the test host name will not search test.foo.example.com if a ~ was used in front of foo.example.com).

You can check your system-resolved configuration with:

resolvectl

If you don't have resolvectl:

systemd-resolve --status

You can now test domain name resolution (assuming some-service.foo.example.com actually exists):

resolvectl query some-service.foo.example.com

If you don't have resolvectl:

systemd-resolve some-service.foo.example.com

If this works, you should have:

some-service.foo.example.com: 192.0.2.22    -- link: tun0

-- Information acquired via protocol DNS in 1.7ms.
-- Data is authenticated: no

In my case, this failed because of:

some-service.foo.example.com: resolve call failed: DNSSEC validation failed: failed-auxiliary

A quick/dirty fix is to disabled DNSSEC:

resolvectl dnssec tun0 off

If you don't have resolvectl:

systemd-resolve -i tun0 \
  --set-dns=192.0.2.53 --set-dns=192.0.2.54 \
  --set-domain=foo.example.com --set-domain=bar.example.com \
  --set-dnssec=off

Hooking with OpenVPN (CLI)

If you are using OpenVPN directly from the CLI (not through Network Manager), you can automate this with the --up options.

For example:

openvpn --config client.ovpn --script-security 2 --up ./manual-config

where ./manual-config is a shell script such as:

#!/bin/sh
set -e
resolvectl dns $dev 192.0.2.53 192.0.2.54
resolvectl domain $dev "~foo.example.com" "~bar.example.com"
resolvectl dnssec $dev off

or

#!/bin/sh
systemd-resolve -i $dev \
  --set-dns=192.0.2.53 --set-dns=192.0.2.54 \
  --set-domain=foo.example.com --set-domain=bar.example.com \
  --set-dnssec=off  # <- Not super nice, but might be needed.

OpenVPN sets the dev environment variable to the name of the VPN TUN/TAP (i.e. virtual) network device (eg. tun0) when calling the hooks.

Don't forget to make this script executable.

Using update-systemd-resolved

An alternative is to use the update-systemd-resolved script (on Debian and Ubuntu it is in the openvpn-systemd-resolved package):

openvpn \
  --config client.ovpn \
  --up /etc/openvpn/update-systemd-resolved \
  --down /etc/openvpn/update-systemd-resolved \
  --down-pre \

This automatically pulls configuration advertised by the VPN server:

  • resolver to used;
  • domain names;
  • whether to disable DNSSEC.

If this fails, the previous section might help you pinpoint to the problem:

  1. check the configuration (resolvectl);
  2. test internal/external names resolution (resolvectl query some-service.foo.example.com)
  3. disable DNSSEC if needed (resolvectl dnssec tun0 off).