How to split your DNS requests when using a VPN

Posted on 2016-11-14
Last updated on 2019-04-02
This tutorial describes how you can split your DNS requests between a local DNS resolver and remote DNS resolvers in order to prevent a DNS leak when you use a VPN connection.

Normally when you set up a local DNS resolver you would keep a list of your local boxes with their associated local IP addresses and then just forward everything else to a remote resolver like OpenDNS or Google, but this approach is ill-advised if you're running a VPN connection on some of your computers as this setup is a security flaw that will cause a DNS leak.

If you run your entire local network through the VPN service for Internet connections then this issue isn't a problem as all requests runs through the VPN. But if you only run a single machine, like your desktop or laptop, through the VPN, while at the same time you use your local DNS resolver for local DNS requests, you have created a security flaw that will cause your DNS requests to be leaked.

A DNS leak means that the true IP address of your Internet connection will be revealed, despite the use of a VPN service to conceal it.

You don't have to run the entire network through the VPN connection in order to prevent a DNS leakage, you can split your DNS requests between local requests and remote requests such that only the local requests goes to the local DNS resolver, while the remote requests goes through the VPN connection.

Several methods exists that can split your DNS requests up between local ones and remote ones, but the easiest to set up is to use Dnsmasq as a DNS splitter on the computers you run the VPN on. A positive side effect of this setup is also that you get DNS caching service on your computers.

In order for this to work correctly the best thing to do is to use a top-level domain on all your local machines, but you can also use something like .internal instead. In this setup I will assume that you are going to use a real domain.

DNS server setup

I assume that on your local DNS server you have a list of computer hostnames with their associated IP addresses.

On Dnsmasq it would look like this:

address=/foo/192.168.1.2
address=/bar/192.168.1.3
address=/baz/192.168.1.4

You need to change this setup into one that uses a top-level domain, whether you use .internal or a real domain doesn't matter, but in this example I am going to use example.com. Then I'll also add reverse lookup for the local machines.

address=/foo.example.com/192.168.1.2
server=/2.1.168.192.in-addr.arpa/192.168.1.1
address=/bar.example.com/192.168.1.3
server=/3.1.168.192.in-addr.arpa/192.168.1.1
address=/baz.example.com/192.168.1.4
server=/4.1.168.192.in-addr.arpa/192.168.1.1

Client setup

resolv.conf

In order to avoid having to type "foo.example.com" every time you want to access machine "foo", you can add the search parameter to your /etc/resolv.conf file. How you do that depends on whether you're manually editing /etc/resolv.conf or you have some kind of network manager handle that.

If you're using some kind of network manager you need to look into how to have it add the search parameter to /etc/resolv.conf. You cannot simply add it manually as your network manager will overwrite /etc/resolv.conf once the computer is rebooted or the connection is restarted.

If you manually handle /etc/resolv.conf, which is preferable in this situation, simply edit it and make sure you have the right search parameter and that all DNS requests goes to your local host on 127.0.0.1.

/etc/resolv.conf needs to look like this:

search example.com
nameserver 127.0.0.1

This means that all DNS requests goes to 127.0.0.1, which is were you will install Dnsmasq and then have that split the requests between your local DNS resolver and a remote one.

The search parameter means that instead of writing ping foo.example.com you can simply write ping foo. The domain part example.com will then automatically be added to foo.

Once you have changed your settings you need to restart the network.

You might consider adding the search example.com part to /etc/resolv.conf on the DNS server on your LAN as well in case you're running other services than DNS on that box. That way you don't need to change scripts or firewall rules from foo to foo.example.com manually.

DNSMasq

Now you have to install Dnsmasq.

On Debian based distributions you can use apt:

# apt install dnsmasq

On Arch Linux:

# pacman -S dnsmasq

Once Dnsmasq has been installed you need to edit /etc/dnsmasq.conf and comment out this part:

# Include all files in a directory which end in .conf
conf-dir=/etc/dnsmasq.d/,*.conf

Then create the file /etc/dnsmasq.d/lan.conf and insert the following:

listen-address=127.0.0.1
# LAN DNS resolver.
server=/example.com/192.168.1.1
rev-server=192.168.1.0/24,192.168.1.1
# VPN remote resolvers.
server=xxx.xx.xxx.xxx
server=xxx.xx.xxx.xxx
no-resolv
no-hosts

You have to change the "xxx.xx.xxx.xxx" part to fit the DNS servers from you VPN provider.

You laso need to make sure that your LAN DNS resolver comes first. In this case I am assuming that your LAN DNS resolver runs on a machine or gateway with the ip address 192.168.1.1.

Next you need to restart Dnsmasq:

# systemctl enable dnsmasq
# systemctl start dnsmasq

Now, when you make a local DNS request the request will first be added the top-level domain example.com to the hostname and then forwarded to your LAN DNS resolver on 192.168.1.1.

You can test with nslookup.

$ nslookup foo

Server:     127.0.0.1
Address:    127.0.0.1#53

Non-authoritative answer:
Name:   foo.example.com
Address: 192.168.1.2

And you can do a reverse lookup too:

$ host 192.168.1.2
foo.example.com has address 192.168.1.2

If you try to resolve a hostname that doesn't belong to your local network you will get an empty reply like this:

$ nslookup bob
Server:     127.0.0.1
Address:    127.0.0.1#53

Non-authoritative answer:
*** Can't find bob: No answer

However, if you try to do a lookup on a domain on the Internet, your request will go through the remote DNS servers:

$ nslookup google.com
Server:     127.0.0.1
Address:    127.0.0.1#53

Non-authoritative answer:
Name:   google.com
Address: 172.217.20.110

Now comes the important part!

In order to verify that your remote DNS requests goes to your remote resolvers through your VPN connection you need to listen to your VPN tunnel interface and verify it manually.

You can do that with tcpdump.

On Debian based distributions you can install tcpdump with apt:

# apt install tcpdump

On Arch Linux using pacman:

# pacman -S tcpdump

In this example I am assuming that your VPN connection runs on the tun0 interface, but you have to check your VPN settings.

Open up a terminal and perform a tcpdump on tun0 as root:

# tcpdump -i tun0 udp port 53

At the same time open up another terminal and do the same with your LAN network interface:

# tcpdump -i enp3s0 udp port 53

Then open up yet another terminal and perform a DNS lookup for a domain on the Internet like yahoo.com:

$ nslookup yahoo.com

On the terminal where you're performing the nslookup you will notice that the request goes to Dnsmasq on 127.0.0.1 as it should, but the terminal running with the tcpdump for the tun0 interface will reveal that the request is actually forwarded to your VPN providers DNS servers:

IP 10.0.10.3.27248 > xxx.xx.xxx.xxx.53: 15420+ [1au] A? yahoo.com. (38)

The IP 10.0.10.3, in this case, is the IP address for the tun0 interface provided by the VPN server and the xxx.xx.xxx.xxx is the main DNS resolver at the VPN provider.

Next you can try a query for a local machine:

$ nslookup foo

Notice the response in the terminal with the tcpdump for the local enp3s0 interface:

12:45:28.634272 IP bumblebee.32876 > _gateway.domain: 6674+ A? foo.example.com.

"bumblebee" is the hostname on the machine doing the nslookup.

You need to be thorough when you validate that the right requests goes to the right DNS server.

That's it!

Feel free to email me any suggestions or comments.