Skip to content

Encrypted DNS

Ferrous DNS supports encrypted DNS both as a client (upstream protocols) and as a server (serving encrypted DNS to your devices).


Upstream Protocols

When Ferrous DNS resolves a query, it can communicate with upstream servers using any of these protocols:

Protocol Standard Description
Plain UDP Traditional DNS, no encryption. Fastest, but exposes queries
Plain TCP DNS over TCP. Used for large responses
DNS-over-HTTPS (DoH) RFC 8484 DNS inside HTTPS. Works through firewalls, hard to block
DNS-over-TLS (DoT) RFC 7858 DNS over TLS. Clean separation from HTTP traffic
DNS-over-QUIC (DoQ) RFC 9250 DNS over QUIC. Lowest latency of encrypted options
HTTP/3 RFC 9114 DoH over HTTP/3 (QUIC). Combines DoH benefits with QUIC performance

Configuring Upstreams

[[dns.pools]]
name = "secure"
strategy = "Parallel"
priority = 1
servers = [
    # DoQ — lowest latency encrypted
    "doq://dns.adguard-dns.com:853",
    "doq://dns.alidns.com:853",

    # DoH — universal compatibility
    "https://cloudflare-dns.com/dns-query",
    "https://dns.google/dns-query",
    "https://dns.quad9.net/dns-query",

    # HTTP/3 (DoH over QUIC)
    "h3://dns.google/dns-query",

    # DoT
    "tls://1.1.1.1:853",
    "tls://8.8.8.8:853",
]

Public Resolver Reference

Provider DoH DoT DoQ
Cloudflare https://cloudflare-dns.com/dns-query tls://1.1.1.1:853
Google https://dns.google/dns-query tls://8.8.8.8:853
Quad9 https://dns.quad9.net/dns-query tls://dns.quad9.net:853
AdGuard https://dns.adguard-dns.com/dns-query tls://dns.adguard-dns.com:853 doq://dns.adguard-dns.com:853
NextDNS https://dns.nextdns.io
CleanBrowsing https://doh.cleanbrowsing.org/doh/security-filter/

Server-Side Encrypted DNS

Ferrous DNS can serve DNS-over-TLS, DNS-over-HTTPS, and DNS-over-QUIC to clients on your network, so devices can connect to Ferrous DNS securely.

The three protocols terminate TLS differently:

  • DoT is terminated by Ferrous DNS itself, using the certificate and key in [server.encrypted_dns].
  • DoH is served as plain HTTP and expects a reverse proxy (nginx, Traefik, Caddy) in front of it to terminate TLS. The DoH endpoint reads X-Real-IP / X-Forwarded-For for correct client attribution, so it must sit behind a proxy that sets those headers.
  • DoQ is terminated by Ferrous DNS itself, same as DoT — QUIC's TLS 1.3 handshake uses the same certificate and key, just over UDP instead of TCP.

Requirements

  • A TLS certificate and private key in PEM format (used by the DoT listener)
  • A reverse proxy terminating HTTPS for DoH
  • Open firewall ports (TCP/853 for DoT, UDP/853 for DoQ — no collision since they're different transports; on the proxy, 443 or custom for DoH)

Configuration

[server.encrypted_dns]
dot_enabled   = true
dot_port      = 853
doh_enabled   = true
doh_port      = 8053       # optional: plain-HTTP port for the proxy to forward to; omit to co-host on web_port
doq_enabled   = true
doq_port      = 853        # UDP port for DNS-over-QUIC (standard: 853)
tls_cert_path = "/data/cert.pem"   # certificate for the DoT and DoQ listeners
tls_key_path  = "/data/key.pem"    # private key for the DoT and DoQ listeners

Each listener binds to [server].bind_address. To put one on a different interface, give it its own address:

[server.encrypted_dns]
dot_bind_address = "[::]"           # DoT dual-stack on every interface
doq_bind_address = "192.168.1.10"   # DoQ only on the LAN address
doh_bind_address = "127.0.0.1"      # DoH reachable only from the local reverse proxy

doh_bind_address is ignored when doh_port is omitted, since /dns-query is then co-hosted on web_port. See Per-listener bind addresses for the details.

DoH TLS termination

tls_cert_path / tls_key_path apply to the DoT listener. The DoH endpoint (/dns-query) is served over plain HTTP — put it behind a reverse proxy that terminates TLS and forwards to doh_port (or to web_port if doh_port is omitted). The cert/key must still load successfully for DoH to start.

Self-Signed Certificate

openssl req -x509 -newkey rsa:4096 -nodes \
  -keyout key.pem -out cert.pem \
  -days 365 \
  -subj "/CN=dns.home.local" \
  -addext "subjectAltName=IP:192.168.1.100,DNS:dns.home.local"

Copy to your data directory and reference in config.

Browsers and DoH with self-signed certificates

For DoH, the certificate is presented by your reverse proxy, not by Ferrous DNS. Browsers will reject DoH requests to a proxy with a self-signed certificate unless the certificate is explicitly trusted by the OS or browser. For browser-based DoH, terminate TLS at the proxy with a Let's Encrypt certificate on a public domain. For DoT on Android and iOS, self-signed certificates are generally accepted.

Let's Encrypt Certificate

If your server has a public domain, use Certbot:

certbot certonly --standalone -d dns.yourdomain.com
# Certificate: /etc/letsencrypt/live/dns.yourdomain.com/fullchain.pem
# Key:         /etc/letsencrypt/live/dns.yourdomain.com/privkey.pem
[server.encrypted_dns]
tls_cert_path = "/etc/letsencrypt/live/dns.yourdomain.com/fullchain.pem"
tls_key_path  = "/etc/letsencrypt/live/dns.yourdomain.com/privkey.pem"

Client Configuration

DNS-over-TLS (DoT)

Settings > Network > Private DNS > Enter hostname:

192.168.1.100
Or with custom hostname:
dns.home.local

Use a .mobileconfig profile or a DNS app like DNSCloak. Set: - Server: 192.168.1.100 - Port: 853 - Protocol: TLS

Under DNS settings, set upstream to:

tls://192.168.1.100:853

/etc/systemd/resolved.conf:

[Resolve]
DNS=192.168.1.100
DNSOverTLS=yes

DNS-over-QUIC (DoQ)

Settings > Network > Private DNS > Enter hostname (Android 13+ supports DoQ automatically when the resolver advertises it via DDR; otherwise use a DoQ-aware app):

192.168.1.100

Point the client's upstream at:

quic://192.168.1.100:853

kdig +quic @192.168.1.100 example.com

DNS-over-HTTPS (DoH)

Settings > Privacy & Security > DNS over HTTPS > Custom:

https://192.168.1.100/dns-query

Settings > Privacy > Security > Use secure DNS > Custom:

https://192.168.1.100/dns-query

curl -s "https://192.168.1.100/dns-query?name=example.com&type=A" \
  --doh-url "https://192.168.1.100/dns-query"
# Test DoT
kdig @192.168.1.100 +tls example.com

# Test DoQ
kdig @192.168.1.100 +quic example.com

# Test DoH
curl -H "accept: application/dns-json" \
  "https://192.168.1.100/dns-query?name=example.com&type=A"

IPv6 Upstreams

Ferrous DNS fully supports IPv6 upstreams:

[[dns.pools]]
name = "ipv6-pool"
strategy = "Parallel"
priority = 1
servers = [
    "https://[2606:4700:4700::1111]/dns-query",   # Cloudflare IPv6
    "https://[2001:4860:4860::8888]/dns-query",   # Google IPv6
]

DNS Name Resolution for Upstreams

Upstream server hostnames are resolved at startup, so you can use domain names directly:

servers = [
    "doq://dns.adguard-dns.com:853",     # resolved at startup
    "https://dns.google/dns-query",       # resolved at startup
]

This avoids bootstrap DNS dependency issues — Ferrous DNS uses the system resolver once at startup to resolve upstream hostnames, then caches the IPs internally.