Skip to content

Document integration of dnss with systemd-resolvd #14

Description

@klogg

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:

  1. 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.
  2. 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.

Activity

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

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions