Sunday, March 17, 2013

Use dnsmasq for separating DNS queries

Automatic network configuration with DHCP is great. But if you need to use multiple separated networks at once, it gets more difficult pretty quickly. For example, my RHEL-6 laptop

  1. connects through wifi to the network at home, which provides internet access
  2. accesses remote systems connected via a VPN
  3. and manages virtual machines that need access to any of those

Now, when NetworkManager connects to the VPN, the DNS-servers for the VPN are added to /etc/resolv.conf with a higher priority than the home network one. This is fine in a lot of circumstances, but that means all domain name
service lookups will go through the VPN first. That's not optimal, and the administrator of the VPN does not need to see all the hostname lookups my laptop is doing either. Also, any lookups for the local network will go through
the VPN, fail there and are retried with the next DNS-server, making queries for the LAN slower than all the others.

The solution sounds simple: Only use the DNS-servers on the VPN for lookups for resources that are on the VPN.

Unfortunately, the configuration is not that simple if it needs to work dynamically. The main configuration file that contains DNS-servers (/etc/resolv.conf) does not have any options to tell that some DNS-servers are to be used for certain domains only. A workaround for this limitation is to use a DNS-server that supports filtering and relaying queries, and have it listen on localhost. This DNS-server is configured in /etc/resolv.conf, and any new network configurations (or removed ones) should not change the configuration in /etc/resolv.conf, but the local DNS-server instead.

This means that my /etc/resolv.conf looks like this:
nameserver 127.0.0.1
search lan.example.net

The minimal /etc/resolv.conf file is also saved as
/etc/resolv.conf.dnsmasq, which is used as a template for restoring the configuration when a VPN service (like OpenVPN) modified it.

The DNS-server for this setup became dnsmasq. This piece of software was already installed on my laptop as a dependency of libvirt, and offers the simple configuration that this setup can benefit from. For this setup, the
libvirt configuration of dnsmasq is not touched, it works fine and with its integrated DHCP-server I am not tempted to break my virtual machines (not now, and not when I install updates).

The configuration to let dnsmasq listen on localhost, and not intervene with libvirt that listens on virbr0 is very minimal as well. My preference is to prevent big changes in packaged configuration files as these may become difficult to merge with updates, so the only change in /etc/dnsmasq.conf that is required is this (newer versions seem to have this by default):
# Include a another lot of configuration options.
#conf-file=/etc/dnsmasq.more.conf
conf-dir=/etc/dnsmasq.d

An additional file in the /etc/dnsmasq.d directory suffices,  /etc/dnsmasq.d/localhost.conf:
no-resolv
no-poll
interface=lo
no-dhcp-interface=lo
bind-interfaces

The default configuration file /etc/dnsmasq.conf contains a good description of these options. It is not needed to repeat them here.

Enabling dnsmasq to start at boot is a prerequisite, otherwise any lookup that uses DNS-servers will fail completely. On my RHEL-6 system, I needed to enable starting of dnsmasq with /sbin/chkconfig dnsmasq on, start
the service with /sbin/service dnsmasq start.

With this current configuration, only hostnames and IP-addresses that are in /etc/hosts are being resolved. Which means, it is difficult to create any network connections outside of the laptop. The next step is to integrate the available connected networks with the dnsmasq configuration.

NetworkManager is used to configure the network on my laptop. This is convenient as it supports WLAN and can connect to the VPN. In order to teach it to write a dnsmasq configuration file for each network that gets setup, I used
an event script, /etc/NetworkManager/dispatcher.d/90-update-resolv.conf:
#!/bin/sh
#
# NetworkManager dispatcher script to prevent messing with DNS servers in the
# LAN.
#
# Author: Niels de Vos 
#

DNSMASQ_RESOLV=/etc/dnsmasq.d/resolv-${CONNECTION_UUID}.conf

function write_dnsmasq_header
{
 if [ ! -e ${DNSMASQ_RESOLV} ]
 then
  echo "# ${DNSMASQ_RESOLV} generated on $(date)" > ${DNSMASQ_RESOLV}
  echo "# Generator: ${0}" >> ${DNSMASQ_RESOLV}
  echo "# Connection: ${CONNECTION_UUID}" >> ${DNSMASQ_RESOLV}
 fi
}

function create_dnsmasq_config_env
{
 local NS

 write_dnsmasq_header

 for NS in ${IP4_NAMESERVERS}
 do
  echo "server=${NS}" >> ${DNSMASQ_RESOLV}
 done
}

function create_dnsmasq_config_from_resolv_conf
{
 local NS
 local DOMAIN=""

 write_dnsmasq_header

 DOMAIN=$(awk '/^domain/ {print $2}' /etc/resolv.conf)
 [ -n "${DOMAIN}" ] && DOMAIN="/${DOMAIN}/"

 for NS in $(awk '/^nameserver/ {print $2}' /etc/resolv.conf)
 do
  # make sure the NS is not from an other config
  grep -q "[=/]${NS}\$" /etc/dnsmasq.d/resolv-*.conf && continue

  echo "server=${DOMAIN}${NS}" >> ${DNSMASQ_RESOLV}
 done
}

function remove_dnsmasq_config
{
 rm -f ${DNSMASQ_RESOLV}
}

function remove_stale_configs
{
 local CONF
 local UUID

 for CONF in /etc/dnsmasq.d/resolv-*.conf
 do
  # in case of a wildcard error
  [ -e "${CONF}" ] || continue

  UUID=$(awk '/^# Connection: / {print $3}' ${CONF})
  if ! ( nmcli -t -f UUID con status | grep -q "^${UUID}\$" )
  then
   rm -f ${CONF}
  fi
 done
}

function reload_dnsmasq
{
 cat /etc/resolv.conf.dnsmasq > /etc/resolv.conf
 [ -n "${DHCP4_DOMAIN_SEARCH}" ] && echo "search ${DHCP4_DOMAIN_SEARCH}" >> /etc/resolv.conf
 # "killall -HUP dnsmasq" is not sufficient for new files
 /sbin/service dnsmasq restart 2>&1 > /dev/null
}

case "$2" in
 "up")
  remove_stale_configs
  create_dnsmasq_config_env
  reload_dnsmasq
  ;;
 "vpn-up")
  remove_stale_configs
  create_dnsmasq_config_from_resolv_conf
  reload_dnsmasq
  ;;
 "down")
  remove_stale_configs
  remove_dnsmasq_config
  reload_dnsmasq
  ;;
 "vpn-down")
  remove_stale_configs
  remove_dnsmasq_config
  reload_dnsmasq
  ;;
esac

This script will write a configuration file like /etc/dnsmasq.d/resolv-0263cda6-edbd-437e-8d36-efb86dcc9112.conf:
# /etc/dnsmasq.d/resolv-0263cda6-edbd-437e-8d36-efb86dcc9112.conf generated on Sun Mar 17 11:57:26 CET 2013
# Generator: /etc/NetworkManager/dispatcher.d/90-update-resolv.conf
# Connection: 0263cda6-edbd-437e-8d36-efb86dcc9112
server=192.168.0.1

The generated configuration file for dnsmasq simply states that there is a DNS-server on 192.168.0.1, which can be used for any query. When the configuration has been written, the dnsmasq daemon is sent a SIGHUP which causes it to reload its configuraion files.

After connecting to a VPN, an other partial configuration file is generated. In this case /etc/dnsmasq.d/resolv-ba76186a-9923-4756-aa8a-19706a4d273c.conf:
# /etc/dnsmasq.d/resolv-ba76186a-9923-4756-aa8a-19706a4d273c.conf generated on Sun Mar 17 11:57:41 CET 2013
# Generator: /etc/NetworkManager/dispatcher.d/90-update-resolv.conf
# Connection: ba76186a-9923-4756-aa8a-19706a4d273c
server=/example.com/10.0.0.1
server=/example.com/10.0.0.2

Similar to the main WLAN connection, this configuration contains two DNS-servers, but these are to be used for the example.com network only.

For me this works in the environments I visit, wifi at home, network cable connected docking station, and several other (non-)public wireless networks.