Unified DNS architecture: integrating dnss with systemd-resolved
The Architecture Goal
In a default state, dnss (a DNS-over-HTTPS proxy) and systemd-resolved fight for control over the standard DNS port (53). When dnss wins, it breaks glibc's ability to resolve local, split-DNS domains via standard applications (ssh, ping).
This setup fundamentally re-architects the stack into a clean pipeline: application -> systemd-resolved -> dnss -> Internet
Step 1: Relocate the dnss Socket
By default, dnss uses Systemd Socket Activation to bind aggressively to the wildcard port *:53. We must move it to an alternate local port so systemd-resolved can reclaim the front-line position.
Action:
sudo systemctl edit dnss.socket
Configuration:
[Socket]
# These empty values are strictly required to clear the default *:53 bindings
ListenStream=
ListenDatagram=
# Bind dnss strictly to a local alternate port
ListenStream=127.0.0.1:5301
ListenDatagram=127.0.0.1:5301
- The Reason: Systemd pre-opens port 53 and passes the file descriptor to the dnss daemon for security (running unprivileged). Because systemd is holding *:53, systemd-resolved silently disables its own UDP stub listener (127.0.0.53:53), causing local application queries to fall straight through to dnss, which forwards them to the public internet (resulting in NXDOMAIN for local services).
- The Consequence: Clearing the ListenStream/Datagram variables releases port 53. Reassigning them to 5301 pushes dnss into the background as a dedicated backend service, freeing up the front door.
Step 2: Configure systemd-resolved as the Master Router
With port 53 free, systemd-resolved needs to be told to use your new dnss backend for all global internet queries, while gracefully handling security protocols.
Action:
sudo vim /etc/systemd/resolved.conf
Configuration:
[Resolve]
# List dnss FIRST, followed by any standard fallback servers
DNS=127.0.0.1:5301
# Change from 'yes' to 'opportunistic'
DNSOverTLS=opportunistic
# Change from 'yes' to 'allow-downgrade'
DNSSEC=allow-downgrade
- The Reason: Global settings in resolved.conf are strictly enforced. Because dnss is listening locally on port 5301, it expects plain-text DNS (which it encrypts after receiving). If DNSOverTLS=yes is set, systemd-resolved attempts a TLS handshake with 127.0.0.1:5301, fails, and drops the connection entirely.
- The Consequence: Setting opportunistic and allow-downgrade tells systemd-resolved to seamlessly pass unencrypted plain-text queries over the safe, internal loopback interface to dnss. dnss then wraps them in DoH. If dnss fails and resolved switches to a public fallback server (like 1.1.1.1), it will automatically upgrade the connection back to strict DoT/DNSSEC.
Step 3: Enforce the Boot Order
Because systemd-resolved now relies entirely on dnss for global internet resolution, starting them out of order creates a race condition that breaks DNS on boot.
Action:
sudo systemctl edit systemd-resolved.service
Configuration:
[Unit]
After=dnss.service
Wants=dnss.service
- The Reason: systemd boots services aggressively and in parallel. If systemd-resolved initializes and attempts to process an OS-level query before the dnss socket is ready to accept traffic on 5301, the network stack temporarily stalls or fails.
- The Consequence: After=dnss.service strictly delays systemd-resolved until dnss reports as fully active. Wants=dnss.service ensures that starting the resolver automatically attempts to pull up the proxy daemon if it was stopped.
Step 4: Apply and Verify
To commit the new architecture to the kernel and systemd daemon:
sudo systemctl daemon-reload
sudo systemctl restart dnss.socket dnss.service
sudo systemctl restart systemd-resolved
Final Traffic Flow Check:
- Local Traffic: application queries 127.0.0.53:53 -> systemd-resolved intercepts it via routing rules -> forwards to your local provider -> connects to the service.
- Global Traffic: application queries 127.0.0.53:53 -> systemd-resolved sees no local route -> forwards to 127.0.0.1:5301 -> dnss encrypts via DoH (port 443) -> securely resolves via the upstream provider.
Unified DNS architecture: integrating dnss with systemd-resolved
The Architecture Goal
In a default state, dnss (a DNS-over-HTTPS proxy) and systemd-resolved fight for control over the standard DNS port (53). When dnss wins, it breaks glibc's ability to resolve local, split-DNS domains via standard applications (ssh, ping).
This setup fundamentally re-architects the stack into a clean pipeline: application -> systemd-resolved -> dnss -> Internet
Step 1: Relocate the dnss Socket
By default, dnss uses Systemd Socket Activation to bind aggressively to the wildcard port *:53. We must move it to an alternate local port so systemd-resolved can reclaim the front-line position.
Action:
sudo systemctl edit dnss.socketConfiguration:
Step 2: Configure systemd-resolved as the Master Router
With port 53 free, systemd-resolved needs to be told to use your new dnss backend for all global internet queries, while gracefully handling security protocols.
Action:
sudo vim /etc/systemd/resolved.confConfiguration:
Step 3: Enforce the Boot Order
Because systemd-resolved now relies entirely on dnss for global internet resolution, starting them out of order creates a race condition that breaks DNS on boot.
Action:
sudo systemctl edit systemd-resolved.serviceConfiguration:
Step 4: Apply and Verify
To commit the new architecture to the kernel and systemd daemon:
Final Traffic Flow Check: