Thursday, September 28, 2023

Using Cloudflare DNS over TLS system-wide on Ubuntu

My current Linux distributions (Ubuntu 23.04 and the Ubuntu-derived Pop!_OS 22.04) use NetworkManager for managing connections, and systemd-resolved for resolving DNS queries.  I’ve set up Cloudflare’s public DNS service with DoT (DNS over TLS) support twice… and I don’t really have a solid conclusion.  Is one “better?”  🤷🏻


  • Per-connection mode with NetworkManager only
  • Globally with systemd-resolved / NetworkManager
  • Useful background info

Per-Connection Mode with NetworkManager

First, I set the IP addresses according to instructions via the NetworkManager GUI, although I could have used the command line as well.  The latter would have been like:

$ nmcli c modify id "AirTubes WiFi" \

And similar for the IPv6 addresses with the ipv6.dns setting.  The “AirTubes WiFi” is the connection name, visible as the “NAME” column of nmcli c show.  Using nmcli c is short for nmcli connection.  For brevity, this post will always use the abbreviation.

The laptop also has an outbound firewall, so I added a rule to allow port 853 out:

$ sudo ufw allow out \
    proto tcp to any port 853 \
    comment "DNS over TLS"

Finally, activating DNS over TLS required the NetworkManager command line (this setting is not available in other interfaces):

$ nmcli c modify id "AirTubes WiFi" \
    connection.dns-over-tls opportunistic

I chose “opportunistic” mode for DoT instead of “yes”, reasoning that for the most part, I’m at home.  I’ll forget about this entire thing if I’m out, and that’s when I need the internet to just work, even if the network is blocking port 853.

Only then did I realize the flaw in per-connection settings: these would not apply if I were actually out. There’s no way a public network would be called “AirTubes WiFi,” so NetworkManager would consider it a different connection.

I put the nmcli settings back to their defaults:

$ nmcli c modify id "AirTubes WiFi" \
    connection.dns-over-tls ""
$ nmcli c modify id "AirTubes WiFi" \
    ipv4.dns ""

And one more time, for ipv6.dns.

Then, I went for global mode.

Globally with systemd-resolved / NetworkManager

The firewall settings to allow port 853 out, above, remained in place.

This approach uses a drop-in file for systemd-resolved, configuring it to do DNS over TLS with CloudFlare by default.  The file looks like this, without comments, and with the DNS line abbreviated:

[Resolve] ... ...

In fact, the DNS line is from the example value for CloudFlare, given in /etc/systemd/resolve.conf.  It was abbreviated here for the blog to display better on phones.

Also note the capitalization here.  DNSoverTLS with lowercase O is wrong, and will be ignored.

Once it’s ready, the file gets copied into place:

$ sudo mkdir /etc/systemd/resolve.conf.d
$ sudo cp local-resolve.conf \

I changed NetworkManager from fully “Automatic” mode to “Automatic (addresses only)”, then left the DNS Servers configuration blank.  It appears that the command-line method to do this (if necessary) is:

$ nmcli c modify id "AirTubes WiFi" \
    ipv4.ignore-auto-dns yes
$ nmcli c modify id "AirTubes WiFi" \
    ipv6.ignore-auto-dns yes

Then, it was a matter of reloading the configurations:

$ sudo systemctl restart systemd-resolved.service
$ nmcli c down id "AirTubes WiFi"
$ nmcli c up id "AirTubes WiFi"

After that, using tcpdump to show traffic to port 853 started reporting packets captured!  I was in business.

The limitation of this approach is that I have to remember to set up any new network connections this way: addresses only, no DNS.  Otherwise, NetworkManager will tell systemd-resolved what DNS settings it wants to use, and they will be applied to the connection.

Useful Background Info

As mentioned in passing above, a NetworkManager setting can be reverted to its default value by setting the empty string:

$ nmcli c modify id "AirTubes WiFi" \
    connection.dns-over-tls ""

A detailed listing of all of a connection’s settings can be seen with:

$ nmcli c show id "AirTubes WiFi" | less

More information about the settings are also available via man page.

$ man nm-settings-nmcli

Getting somewhat unrelated, there is a command to search man pages for keywords.  This is how I discovered that there is an nmtui (text user interface) command, which gives a terminal-based menu similar to the GUI.  Anyway, to search the man pages for NetworkManager:

$ man -k NetworkManager

Firewall rules can be restricted by IP.  It was straightforward enough to do IPv4, but for IPv6, the address needs to be “complete” (note the double colon):

$ sudo ufw allow out proto tcp \
    to port 853 \
    comment 'CloudFlare DoT'
$ sudo ufw allow out proto tcp \
    to 2606:4700:4700::/112 port 853 \
    comment 'CloudFlare DoT v6'

I chose the network masks so that one rule would cover both respective addresses, without allowing the port to the entire Internet.  (Discussion of network masks and CIDR notation for them—the /15 and /112—are out of scope for this blog post.)

When using systemd-resolved for name resolution, the status can be inspected with its own command:

$ resolvectl status

Here, the output should say things like +DNSOverTLS, and there shouldn’t be any per-link overrides of the “Global” section.  It should be more like (reformatted to reduce width):

    Protocols: -LLMNR -mDNS +DNSOverTLS
resolv.conf mode: stub
    DNS Servers

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

Link 3 (wlx........)
Current Scopes: none
     Protocols: -DefaultRoute +LLMNR -mDNS
        +DNSOverTLS DNSSEC=no/unsupported

I hope that covers it!

No comments: