[GH-ISSUE #14] Document integration of dnss with systemd-resolvd #13

Open
opened 2026-03-02 23:35:58 +03:00 by kerem · 0 comments
Owner

Originally created by @klogg on GitHub (Feb 20, 2026).
Original GitHub issue: https://github.com/albertito/dnss/issues/14

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.
Originally created by @klogg on GitHub (Feb 20, 2026). Original GitHub issue: https://github.com/albertito/dnss/issues/14 # 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:** ```console sudo systemctl edit dnss.socket ``` **Configuration:** ```bash [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:** ```console sudo vim /etc/systemd/resolved.conf ``` **Configuration:** ```bash [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:** ```console sudo systemctl edit systemd-resolved.service ``` **Configuration:** ```bash [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: ```console 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.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/dnss#13
No description provided.