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.