Dynamic between DNS-over-HTTPS, DNS-over-TLS, DNS-over-QUIC, with DNS-over-IP Fallback

If something fails, for whatever reason, this will move right onto the next portal, and the next, and finally fall back to regular DNS over IP.

You will need: dnsproxy and dnsmasq in your system software list.

First a before and after (what I started with, and what I am sharing now).

Here is the before, which I used for simple DNS-over-IP. This can be used with any DNS-over-IP, with the optional ID if needed (for example, AdGuard DNS).

# ----------------------------------------
# 🛡️ DNS (via dnsmasq)
# ----------------------------------------

# 🌐 Enable dnsmasq (DNS Forwarder & DHCP Server)
# Use this if you want custom DNS. For example, AdGuard DNS.

services.dnsmasq.enable = true;

# ⚙️ **dnsmasq Settings** - Custom DNS Setup
# To use: Uncomment and modify below.

# services.dnsmasq.settings = {
   no-resolv     = true;      # ❌ Don't use system DNS
   bogus-priv    = true;      # 🔒 Block private IPs
   strict-order  = true;      # 📐 Use servers in order
   server  = [ "94.140.14.49" "94.140.14.59" ]; # Replace with preferred DNS servers.

# Optional: Add your DNS ID if needed (for example, AdGuard DNS).
   add-cpe-id    = "abc123"; # ⬅️ Replace with your ID here.
 };

This worked, and while I liked it, I wanted to have an encrypted DNS. That is how I ended up with this:

# 🛡️ **Encrypted DNS Proxy using dnsproxy**
# This configuration secures your DNS traffic using DNS-over-HTTPS (DoH), DNS-over-QUIC (DoQ), or DNS-over-TLS (DoT).
# 💡 Tip: You can uncomment one protocol in the `upstream` list, or uncomment more for dynamic fallback.

services.dnsproxy = {
  enable = true;  # Turn on the dnsproxy service
  settings = {
    listen-addrs = [ "127.0.0.1" ];  # Listens on your local computer (127.0.0.1).
    listen-ports = [ 53 ];  # Default DNS port (port 53). This will handle DNS requests.

    # 🌐 **Encrypted DNS protocols (choose one or more)**
    upstream = [
      "https://d.adguard-dns.com/dns-query/abc123"  # DNS-over-HTTPS (port 443).
      "quic://abc123.adguard-dns.com"  # DNS-over-QUIC (port 784).
      "tls://abc123.d.adguard-dns.com"  # DNS-over-TLS (port 853).
      "127.0.0.1:5353"  # Local fallback to dnsmasq (important for reliability)
    ];
  };
  flags = [ "--verbose" ];  # Adds verbose logging to help debug (optional).
};

# ⚠️ Important Note:
# This section uses port 53 on localhost, which requires root privileges (administrator rights).
# We give the system permission to bind to this port using special capabilities.

systemd.services.dnsproxy.serviceConfig = {
  AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];  # Allows binding to privileged ports
  CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];  # Limits the capabilities for security
};

# 🔄 **Configure system DNS with systemd-resolved**
# Use dnsproxy for DNS queries instead of default system DNS settings.

environment.etc."systemd/resolved.conf.d/dnsproxy.conf".text = ''
  [Resolve]
  DNS=127.0.0.1  # Use the local dnsproxy service for DNS requests.
  FallbackDNS=   # Empty to avoid using fallback DNS.
  DNSStubListener=no  # Disables systemd's DNS listener.
'';

services.resolved.enable = true;  # Enable systemd-resolved (required for network management).
networking.nameservers = [ "127.0.0.1" ];  # Set dnsproxy (local server) as the DNS resolver.
networking.networkmanager.dns = "systemd-resolved";  # Tell NetworkManager to use systemd-resolved for DNS.

# ⚙️ **Fallback DNS with dnsmasq (if encrypted DNS fails)**
# If dnsproxy cannot connect to the encrypted DNS server, this section uses dnsmasq as a fallback DNS server.
# This ensures your system will always have a working DNS server.

services.dnsmasq = {
  enable = true;  # Turn on dnsmasq to handle fallback DNS requests.
  settings = {
    port = 5353;  # Local DNS port for dnsmasq.
    no-resolv = true;  # Don't use default system DNS servers.
    bogus-priv = true;  # Block responses from private IP ranges.
    strict-order = true;  # Ensure DNS requests are sent to servers in order.
    server = [
      "94.140.14.49"  # Example IPv4 fallback DNS server.
      "94.140.14.59"  # Another fallback DNS server.
      "2a10:50c0:c000::"  # Example IPv6 fallback server.
      "2a10:50c0:c000::1"  # Another IPv6 fallback server.
    ];
    # Optional ID (for example, AAdGuard DNS)
    add-cpe-id = "abc123";  # ⬅️ Replace with your ID here.
  };
};

If you are using avahi for local discovery, you also want to add:

  environment.etc."nsswitch.conf".text = ''
  hosts: files mdns4_minimal [NOTFOUND=return] dns mdns6
  passwd: files systemd
  group: files systemd
  shadow: files
 '';

I am not sharing this as a guide. I want people to review this.

The only “odd” thing was AdGuard DNS took a while to log things. I’m unsure why that was, but it took more than a few hours after moving from my old config to my new, before I noticed anything in the query. Even though I had internet right away, and everything was working. I even tested this 1 by 1 without the IP fall back, and it worked.

1 Like