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 | — |
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-Forfor 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:
Or with custom hostname:Use a .mobileconfig profile or a DNS app like DNSCloak. Set: - Server: 192.168.1.100 - Port: 853 - Protocol: TLS
DNS-over-QUIC (DoQ)¶
DNS-over-HTTPS (DoH)¶
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.