Scenario: We have a Linux/BSD laptop connected to a couple of corporate VPNs, each of which should only receive traffic intended for its particular domain (DNS queries in particular), while the DHCP settings provided by the current wifi/ethernet network are used for everything else.
Apparently, systemd does this reasonably well. But as it turns out, there are small circles of crazy people who have not fallen to the Dark Side yet, so let’s try doing it with simpler tools and, most importantly, taking the opportunity to learn something.
The problem Link to heading
This guy: /etc/resolv.conf.
…? Link to heading
Ok, let’s go back a little bit. What happens when a program tries to resolve
some domain name like example.com by calling one of the C functions
(gethostbyname(3), getaddrinfo(3), etc)? The details depend on the concrete
implementation of the C library1 , but if the name cannot be resolved
directly by using the /etc/hosts file, it will surely end up searching in the
standard resolver configuration file (/etc/resolv.conf) for some DNS server
(nameserver) to query.
The problem is that multiple entities can write to /etc/resolv.conf for different
purposes:
- Network config daemons such as NetworkManager, to reflect settings received via DHCP
- VPN software such as
wg-quickor OpenVPN, to configure internal DNS resolvers - The user, to specify its preferred DNS server
- …
If each of them writes down its own nameserver, which one will be used?
Yeah, the first one.2
Which is almost always not what we want.
This mechanism is quite ancient and we’re not going to change the way it works, but fortunately we can build upon it.
First we’re going to move this dispatching logic to our own local DNS resolver. We’ll be using dnsmasq in Linux which is quite lightweight and enough for our needs, but the same could be archieved with Unbound or named, to name a few.
And then, we will need resolvconf to
receive and join the different resolv.conf’s provided by external sources, make some
decissions involving priorities, default resolvers and so on, and provide this
information to dnsmasq via configuration files. Basically, it will configure
dnsmasq to forward to each resolver all queries belonging to the same
domain or search domain as it.
================================================================================
(ns: 192.168.0.1)
NetworkManager ---.
|
| .------------.
wg-quick ---------+---> | resolvconf |
(ns: 10.10.0.1) | `------------`
| |
OpenVPN ----------` |
(ns: 10.255.0.10) / \
/ \
/ \
| |
v v
/etc/dnsmasq-conf.conf /etc/dnsmasq-resolv.conf
o o
\ |
\ |
\ | (dispatch to correct resolver)
\| /---> 192.168.0.1
v |
.-------. .---------. |
| glibc | ---> /etc/resolv.conf ---> | dnsmasq |---+---> 10.10.0.1
`-------` `---------` |
|
\---> 10.255.0.10
================================================================================
To recap: dnsmasq will proxy local DNS requests to the correct resolver,
taking into account the information provided to resolvconf by third-party
software wanting to add its own resolver(s).
As always, refer to the manpages of each of this services to learn more:
resolv.conf(5), resolvconf(8), resolvconf.conf(5) (yeah, those three are
different things), dnsmasq(8).
resolvconf Link to heading
Sample /etc/resolvconf.conf, using a corporate vpn wg0:
# Configure dnsmasq as the local proxy resolver. This will take control of
# /etc/resolv.conf by setting 127.0.0.1 as the first and only nameserver.
name_servers=127.0.0.1
# Tell resolvconf to generate configuration files for dnsmasq.
dnsmasq_resolv=/etc/dnsmasq-resolv.conf
dnsmasq_conf=/etc/dnsmasq-conf.conf
# wg0: corporate VPN:
# - private: only use wg0 DNS resolvers with its own domains
# - inclusive: by default, wg-quick marks Wireguard interfaces as exclusive,
# which forces ALL queries to it. We disable it so that only queries for that
# domain will be sent to its server
private_interfaces=wg0
inclusive_interfaces=wg0
dnsmasq Link to heading
Configure /etc/dnsmasq.conf to read the configuration files output by
resolvconf:
resolv-file=/etc/dnsmasq-resolv.conf
conf-file=/etc/dnsmasq-conf.conf
The first one is another resolv.conf generated by resolvconf instead of the
common one (the one in /etc), which dnsmasq should use to avoid getting into a
loop (dnsmasq trying to call itself). The second one is a dynamically-generated
dnsmasq config intended to configure resolvers specific to concrete domains.
Clients Link to heading
resolvconf needs to be called by each program that would normally want to edit
/etc/resolv.conf, so you may need to configure each one of them in a different
way:
NetworkManager Link to heading
Edit /etc/NetworkManager/conf.d/rc-manager.conf:
[main]
rc-manager=resolvconf
Restart NetworkManager to apply the settings.
Wireguard Link to heading
If you’re using Wireguard via wg-quick, it calls resolvconf by default, so you
do not need to configure anything, except maybe tuning resolvconf.conf to
(un)mark the interface as private or exclusive (see the example
above).
OpenVPN Link to heading
All hail update-resolv-conf.sh.
Checking everything works Link to heading
You can ask resolvconf what configurations it has received so far:
$ resolvconf -l # resolv.conf's sent by each source
$ resolvconf -lv # joined info
And see what it has decided to pass to dnsmasq:
$ cat /etc/dnsmasq-conf.conf
$ cat /etc/dnsmasq-resolv.conf
Finally, try attaching tcpdump to each network interface to see what DNS
queries are sent by each of them:
# tcpdump -ni wlo1 udp port 53
# tcpdump -ni tap0 udp port 53
...