Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save jobbler/2acf8fb2f563e87f7646f71a394922e8 to your computer and use it in GitHub Desktop.
Save jobbler/2acf8fb2f563e87f7646f71a394922e8 to your computer and use it in GitHub Desktop.
Caching and Conditional Nameserver with systemd-resolved.md

Introduction

During my daily work, I need to resolve server/domains against several different nameservers based upon network connections. Normally Network Manager and the system handles this quite well based upon the DNS provided by the dhcp on the connection.

However, I have some unique cases where I need to use multiple name servers on a single connection for a single connection. This is not normally something to worry about because dns forwards to other name servers, if configured to, until a response is returned.

I need to use some name servers that are easily and quickly updated for lab use. These are not controlled or even known about by IT. They are used for DNS lookup of OpenShift clusters.

This is my driving need for the configuration in this document. An added bonus is a little quicker resolution time that adds up tremendously (especially on web pages).

Environment

Current

I have Fedora 39 running on my laptop. During a normal work day, I connect to my companies VPN (VPN-1) for access to equipment and internal company resources. Once I connect to VPN-1, my laptop uses the internal DNS servers provided by the VPN connection.

On this same VPN, I access a lab that contains its own DNS servers for equipment on its network. These are a subdomain (lab1.company.example) of the company domain (company.example). Inside this lab I have a set of OpenShift clusters that I reinstall and change names on constantly. Instead of bothering the lab admin to constantly make dns changes for me, I configured another dns server in the lab to provide dns for/to my clusters. I can set short TTLs and update this as often as I want.

I also have another lab that I connect to via VPN-2. This lab has its own DNS servers for me to use.

graph BT;

laptop[Local Laptop or Desktop]
lab1dns[DNS: lab1.company.example]
lab2dns[DNS: lab2.example]
clusterdns[DNS: example.org clusters]
pubdns[DNS: Public Resolution]
companydns[DNS: Company Resolution]

laptop --- lab1dns;
laptop --- clusterdns;
laptop --- lab2dns;
laptop --- pubdns;

laptop -.- companydns;
laptop -.- pubdns;
%% laptop -. Company Resolution .- pubdns;


subgraph VPN-2;
  subgraph Lab-2;
    lab2dns;
    end
end

subgraph VPN-1;
  companydns;
  subgraph Lab-1;
    lab1dns;
    clusterdns;
  end
end

subgraph Internet
    pubdns;
end

Loading

Proposed Configuration

With the above configuration, Network Manager does not automatically know all the DNS servers I need to access.

In the past I have configured a local Caching Bind server on my laptop and just pointed my laptop to the local instance for all name resolution. This worked great until the introduction of systemd-resolved. It is still doable, just a little more involved.

I have performed this using two different caching nameservers running on my local host, dnsmasq and bind9 (named). I personally prefer using bind9. There are a nuances with bind9 that make it difficult to work with sometimes. These seem to be the syntax in the zone database files. But I am not creating any zone database files. I am only configuring caching and forwarding for specific domains. So this is easy to setup in my opinion.

A lot of people use dnsmasq and really like it. It provides dns resolution and dhcp services if configured. And the configuration for dnsmasq is pretty easy. Adding dns entries and forwarding entries are very easy to do.

There is one slight difference, and for me its a big one. Its the way they treat nameservers that cannot be reached. If bind9 cannot find a forward nameserver, then it falls back to the default forward nameserver. If dnsmasq cannot find a forward nameserver, then it returns no results.

This behavior is most noticeable when using VPNs. For example: when not connected to a VPN, I want the public nameservers such as google to resolve entries for my company's domain. This is needed for me to connect to the company VPN.

But when I am connected to the VPN, I want my company's internal nameserver to resolve entries for the company domain (and only the company domain). This is because the internal servers may direct me to internal ip addresses instead of public ones. These internal servers may be quicker for me or provide different features when connected from the VPN with an internal IP address.

For this scenario, I just need to specify my company's internal nameservers for my company's domain in the bind9 configuration file and I am done. Bind9 will just fall back to the public nameserver when it cant reach my company's internal nameservers.

But dnsmasq seems to just stop trying when it cannot reach the internal nameserver I specified for the company's domain. There is a workaround, you can add dynamic entries to dnsmasq through the DBUS. So I can not include configuration in the dnsmasq.conf file for my company's domain. This allows me to connect to the VPN. But once I connect to the VPN, I can have NetworkManager dynamically create the entries (and remove them when I disconnect).

But there is a slight issue with this method, if I restart dnsmasq, the dynamic entries go away and I must manually run the dispatcher script to add them back. It is annoying and I dont like to be annoyed.

So I prefer Bind9 over dnsmasq for this. And I am not an expert on dnsmasq, in fact I just started using it. So there may be a setting to change its behavior. But I need to get some work done and will stick with what I know. But I will show both methods here.

Here is what needs to be done when using Bind9.

  1. Configure systemd-resolved to send lookups to my locally hosted bind9 (named) instance.
  2. Configure a local bind9 (named) instance to function as both a caching name server and a conditional name server that will send DNS queries to the appropriate name servers.
  3. Configure the interfaces to not use the DNS provided by DHCP.

Here is what needs to be done when using dnsmasq.

  1. Configure systemd-resolved to send lookups to my locally hosted dnsmasq instance.
  2. Configure a local dnsmasq instance to function as both a caching name server and a conditional name server that will send DNS queries to the appropriate name servers.
  3. Configure the interfaces to not use the DNS provided by DHCP.
  4. Configure NetworkManager-dispatcher to add name server entries into dnsmaqs using the D-bus. This is needed so that dnsmasq can use the internet name severs to resolve the company domain name when not connected to the VPN. And then when the VPN is connected, NetworkManager-dispatcher will add entries so that internal name servers are used to resolve company domain name.

Configure systemd-resolved

The following configuration tells systemd-resolved to forward queries for all unknown routes to 127.0.0.1 on port 5353, which is the bind9 or dnsmasq instance.

/etc/systemd/resolved.conf

# Use the following DNS server
DNS=127.0.0.1:5353

# Route all domains that are not know to the above DNS server
Domains=~.

# Disable LLMNR support
LLMNR=false

# Do not cache failed lookups
Cache=no-negative

Systemd-resolved need to be restarted after the changes.

systemctl restart systemd-resolved

Configure the Interfaces

Even though systemd-resolved has been restarted, the dns queries will still go to the name servers provided by DHCP on the interfaces. This can be seen by running the resolvedctl status command.

# resolvectl status

Global
         Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
  resolv.conf mode: stub
Current DNS Server: 127.0.0.1:5353
       DNS Servers: 127.0.0.1:5353
        DNS Domain: ~.

Link 2 (enp0s31f6)
    Current Scopes: none
         Protocols: -DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
       DNS Servers: 192.168.1.2
        DNS Domain: home.example

Link 4 (wlp0s20f3)
    Current Scopes: none
         Protocols: -DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported

Link 15 (tun0)
    Current Scopes: none
         Protocols: -DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
       DNS Servers: 10.0.0.2 10.0.0.3
        DNS Domain: company.example

You can see the Global configuration shows the changes. Remember the changes affect only unknown domains (~.).

Now configure each interface to ignore DNS provided by DHCP.

Use nmcli connection show to view the interfaces on the system.

# nmcli c

NAME              UUID                                  TYPE      DEVICE        
vpn-1             e69b124b-50a8-4458-8859-112233445566  vpn       eno1
vpn-2             e69b124b-50a8-4458-8859-334455667788  vpn       eno1
wired             fd7f52a4-bbaf-3ba0-847e-223344556677  ethernet  eno1

Now use nmcli to modify the connection to ignore auto dns.

nmcli connection modify wired ipv4.ignore-auto-dns true

nmcli connection modify vpn-1 ipv4.ignore-auto-dns true

nmcli connection modify vpn-2 ipv4.ignore-auto-dns true

The interfaces will need to be brought down and then up again for the changes to take affect.

The interfaces should no longer show DNS information

# resolvectl status

Global
         Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
  resolv.conf mode: stub
Current DNS Server: 127.0.0.1:5353
       DNS Servers: 127.0.0.1:5353
        DNS Domain: ~.

Link 2 (enp0s31f6)
    Current Scopes: none
         Protocols: -DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported

Link 4 (wlp0s20f3)
    Current Scopes: none
         Protocols: -DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported

Link 15 (tun0)
    Current Scopes: none
         Protocols: -DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported

Configuring bind9 (named)

Install bind9 and the bind-utils.

dnf install bind bind-utils

Configure the named configuration file.

//
// named.conf
//
// Provided by Red Hat bind package to configure the ISC BIND named(8) DNS
// server as a caching only nameserver (as a localhost DNS resolver only).
//
// See /usr/share/doc/bind*/sample/ for example named configuration files.
//

options {
	listen-on port 5353 { any; };

	directory 	"/var/named";
	dump-file 	"/var/named/data/cache_dump.db";
	statistics-file "/var/named/data/named_stats.txt";
	memstatistics-file "/var/named/data/named_mem_stats.txt";
	allow-query     { any; };

        dnssec-validation no;
        recursion yes;

        disable-empty-zone "10.in-addr.arpa";

        forward only;

        forwarders {
            8.8.8.8;
            4.4.4.4;
        };

	pid-file "/run/named/named.pid";
	session-keyfile "/run/named/session.key";

	/* https://fedoraproject.org/wiki/Changes/CryptoPolicy */
	include "/etc/crypto-policies/back-ends/bind.config";
};

logging {
        channel default_debug {
                file "data/named.run";
		print-time yes;
		print-category yes;
		print-severity yes;
                severity dynamic;
        };
};


// lab1 forward
zone "lab1.company.example" {
        type forward;
        forwarders {
                10.0.80.2;
                10.0.80.3;
        };
};

// lab1 reverse
zone "80.0.10.in-addr.arpa" {
        type forward;
        forwarders {
                10.0.80.2;
                10.0.80.3;
        };
};

// cluster in lab1 forward
zone "example.org" {
        type forward;
        forwarders {
                10.0.100.39;
        };
};

// cluster in lab1 reverse
zone "100.0.10.in-addr.arpa" {
        type forward;
        forwarders {
                10.0.100.39;
        };
};

// lab2 forward
zone "lab2.company.example" {
        type forward;
        forwarders {
                192.168.13.11;
        };
};

// lab2 reverse
zone "13.168.192.in-addr.arpa" {
        type forward;
        forwarders {
                192.168.13.11;
        };
};

// company internal nameservers forward
zone "company.example" {
        type forward;
        forwarders {
                10.0.100.39;
        };
};

// company internal nameservers reverse
zone "100.0.10.in-addr.arpa" {
        type forward;
        forwarders {
                10.0.100.39;
        };
};

After dnsmasq is configured, we need to enable it and make sure it starts on boot.

systemctl enable named --now

If we are connected to everything, we can test resolution by querying the dnsmasq instance directly for hosts that should be resolved by the different name servers.

dig @localhost -p 5353 server.company.example

Configuring dnsmasq

First, there are two ways to use dnsmasq - using NetworkManager or as an independant instance. I chose to use an independant instance. I dont really have any real reason to use and independant instance, except I am old and still believe a program should do one thing and do it well. And Network Manager and Systemd seem to be expanding to do multiple things and that is where I will leave my comments.

I installed dnsmasq and bind-utils.

dnf install dnsmasq bind-utils

The main dnsmasq.conf configuration file has the following non-commented configuration.

/etc/dnsmasq.conf

# User and group to run as
user=dnsmasq
group=dnsmasq

# Only accept lookups from systems on the same subne
local-service

# The port to listen on
port=5353

# Location and extensions of configuration files
conf-dir=/etc/dnsmasq.d,.rpmnew,.rpmsave,.rpmorig

I broke some of the configuration I may be more likely to modify over time into a second file and placed it under the /etc/dnsmasq.d directory. It is important to make sure this is the first file to be read in. Prepending the 00- to the beginning of it helps ensure it will run first.

/etc/dnsmasq.d/00-main.conf

# Log the dns queries - great for troubleshooting and verifying things are working as expected.
# Can turn this off once satisfied everything works.
log-queries

# Ignore the /etc/resolve.conf file.
no-resolv

# Ignore the /etc/hosts file.
no-hosts

# Do not cache negative lookups
no-negcache

# Allow access to dnsmasq using the D-bus
enable-dbus

My final config file is also in the /etc/dnsmasq.d directory I make sure it runs after the other file by prepending 10-.

/etc/dnsmasq.d/10-conditional.conf


# Public dns servers - These are the default dns servers used for anything not specified here.
server=8.8.8.8
server=8.8.4.4

# Lab 1
server=/lab1.company.example/10.0.80.2
server=/lab1.company.example/10.0.80.3
rev-server=10.0.0.0/16,10.0.80.2
rev-server=10.0.0.0/16,10.0.80.4

# Cluster in Lab 1
server=/example.org/10.0.100.39
rev-server=10.0.100.39/32,10.0.100.39
rev-server=10.0.100.40/32,10.0.100.39
rev-server=10.0.100.41/32,10.0.100.39
rev-server=10.0.100.42/32,10.0.100.39
rev-server=10.0.100.43/32,10.0.100.39
rev-server=10.0.100.44/32,10.0.100.39

# Lab 2
server=/lab2.example/192.168.13.11
rev-server=192.168.13.0/24,192.168.13.11
rev-server=192.168.59.0/24,192.168.13.11

# These are commented out and then dynamically added through d-bus
#server=/company.example/10.0.0.2
#server=/company.example/10.0.0.3
#rev-server=10.0.0.0/8,10.0.0.2
#rev-server=10.0.0.0/8,10.0.0.3

After dnsmasq is configured, we need to enable it and make sure it starts on boot.

systemctl enable dnsmasq
systemctl start dnsmasq

If we are connected to everything, we can test resolution by querying the dnsmasq instance directly for hosts that should be resolved by the different name servers.

dig @localhost -p 5353 server.company.example

Configure NetworkManager-dispatcher

Now everything works great, all the resolution show be working for the most part.

But resolution for the company.example domain is always getting resolved by the public internet nameservers. They need to resolve using the companies web servers when connected to VPN-1.

If these are set in the dnsmasq.conf file, then they won't resolve when not connected to the VPN. Which means we wont connect to the VPN since the VPN's FQDN is vpn.company.example and the servers cannot be reached until we connect. Kind of an egg and a chicken thing going on.

However, NetworkManager has a dispatcher service that can run scripts on connection changes.

First the service needs to be started if it is not already running.

systemctl start NetworkManager-dispatcher.service

Create the following script in the /etc/NetworkManager/dispatcher.d directory that will run on connection actions.

/etc/NetworkManager/dispatcher.d/10-global-vpn

#! /bin/bash

interface=$1
event=$2

#echo "$interface received $event" | systemd-cat -p info -t dispatch_script
#echo "$interface received $event" > /tmp/q

## Global VPN
[[ $1 == "tun0" ]] && [[ $2 == "up" ]] && {
  dbus-send --system --print-reply \
    --dest=uk.org.thekelleys.dnsmasq \
      /uk/org/thekelleys/dnsmasq \
      uk.org.thekelleys.dnsmasq.SetServers \
      uint32:167772162 string:"company.example" \
      uint32:167772163 string:"company.example"

  dbus-send --print-reply --system \
    --dest=uk.org.thekelleys.dnsmasq \
      /uk/org/thekelleys/dnsmasq \
      uk.org.thekelleys.dnsmasq.ClearCache

}

[[ $1 == "tun0" ]] && [[ $2 == "down" ]] && {

  dbus-send --system --print-reply \
    --dest=uk.org.thekelleys.dnsmasq \
      /uk/org/thekelleys/dnsmasq \
      uk.org.thekelleys.dnsmasq.SetServers \
      uint32:

  dbus-send --print-reply --system \
    --dest=uk.org.thekelleys.dnsmasq \
      /uk/org/thekelleys/dnsmasq \
      uk.org.thekelleys.dnsmasq.ClearCache

}

echo

This script checks for the interface VPN-1 interface, tun0 to go either up or down. If it goes up, then the script inserts a server=/company.example/10.0.0.2 and server=/company.example/10.0.0.3 line into the dnsmasq running configuration usig D-bus and then clears dnsmasq's cache. If the interface goes down, the script clears all configuration inserted via the D-bus and clears the cache.

Note the lines in the above file that contain uint32:. These lines contain the integer version of the name servers IP address and the domain name. These need to be replaced based upon the name server(s) and domain.

There are tools on the internet that can convert an IP address into its integer form, or the following script can be used.

ip2int.sh

#! /bin/bash

for ip in $@
do
  IFS='.' read -r first_octet second_octet third_octet fourth_octet <<< $ip
  iip=$(( \
       (first_octet  * 256 ** 3) \
     + (second_octet * 256 ** 2) \
     + (third_octet  * 256 ** 1) \
     + (fourth_octet * 256 ** 0) ))

  echo "uint32:$iip"
done

# ip2int.sh 10.0.0.2 10.0.0.3

uint32:167772162
uint32:167772163

Verify and Troubleshoot

When using port 5353, I needed to set the following SELinux boolean.

  • setsebool -P nis_enabled 1

Use the following to watch the dns queries.

  • View that the dispatcher script is running journalctl -xefu NetworkManager-dispatcher

  • View the actions of dnsmasq journalctl -xefu dnsmasq

  • View the actions of systemd-resolved resolvectl monitor

  • View the status of systemd-resolved's DNS resolvectl status dnsmasq

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment