DoT, DoH, DoQ: encrypted DNS
Encrypted DNS is orthogonal to dnssec: DNSSEC proves a record is authentic, encrypted DNS hides who asked. See anatomy-of-a-query for where each layer sits.
Asks the same question over TLS on port 853 instead of plain UDP.
dig @1.1.1.1 example.isitdns.net A +tlsGO NOERROR, and a SERVER line ending 853 and (TLS)
NO GO a connection failure, which means port 853 is blocked on your path
The three protocols at a glance#
| DoT | DoH | DoQ | |
|---|---|---|---|
| RFC | 7858 | 8484 | 9250 |
| Port | 853 (TCP) | 443 | 853 (UDP) |
| Transport | TCP + TLS | HTTPS | QUIC (UDP) |
| Hides on the wire | yes | yes (looks like HTTPS) | yes |
| Blends with web traffic | no (distinct port) | yes (port 443) | no (distinct port) |
| Per-query latency | TCP + TLS handshake | TCP + TLS + HTTP framing | 0-RTT possible |
| Connection reuse | yes | yes | yes |
Testing DoT with kdig#
Cloudflare's 1.1.1.1 serves DoT at port 853. Query it with kdig (from knot-dnsutils) or with dig 9.18 or later, which added +tls in BIND 9.18. Both print the transport: kdig in its banner, dig 9.18+ as (TLS) on the ;; SERVER: line (measured with DiG 9.20.18, 2026-09-23: ;; SERVER: 1.1.1.1#853(1.1.1.1) (TLS)).
kdig @1.1.1.1 +tls cloudflare.comInstall via apt install knot-dnsutils (Debian/Ubuntu) or brew install knot (macOS).
nslookup cannot send queries over DoT. Use
kdig.
Stock macOS
dig(BIND 9.10.x) does NOT support+tlsor+https. An option error means yourdigpredates 9.18; installkdig.
The output looks like plain dig with two extra tells: a ;; TLS session (TLS1.3)-... line at the top, and a footer that reads ;; From 1.1.1.1@853(TLS). To see the certificate the resolver presents on port 853:
openssl s_client -connect 1.1.1.1:853 -servername cloudflare-dns.com \
-verify_hostname cloudflare-dns.com </dev/null 2>/dev/null \
| grep -E "subject=|issuer=|Verify"subject=C=US, ST=California, L=San Francisco, O=Cloudflare, Inc., CN=cloudflare-dns.com
issuer=C=US, ST=Texas, L=Houston, O=SSL Corp, CN=SSL.com SSL Intermediate CA ECC R2
Verify return code: 0 (ok)Match on the verify code (RFC 8310 §8.1), never the issuer: it changes whenever Cloudflare rotates CAs, and field spacing is only an OpenSSL-version artefact. Cloudflare's own doc names 1.1.1.1/1.0.0.1 on port 853 and, for a client that needs a hostname, one.one.one.one; cloudflare-dns.com is the name the certificate carries, which is why it verifies here (DNS over TLS).
nslookup cannot inspect TLS certificates. Use
openssl s_clientas shown above.
Testing DoH#
The JSON-over-HTTPS variant (application/dns-json, a non-RFC extension offered by Cloudflare and Google) is the easiest to test from any HTTP client:
curl -s -H 'accept: application/dns-json' \
'https://cloudflare-dns.com/dns-query?name=cloudflare.com&type=A'{
"Status": 0,
"TC": false,
"RD": true,
"RA": true,
"AD": true,
"CD": false,
"Question": [{"name": "cloudflare.com", "type": 1}],
"Answer": [
{"name": "cloudflare.com", "type": 1, "TTL": 295, "data": "104.16.133.229"},
{"name": "cloudflare.com", "type": 1, "TTL": 295, "data": "104.16.132.229"}
]
}(The live response arrives as a single unformatted line; it is pretty-printed here. The TTL is the resolver's cache countdown, so your number will differ on every run.)
"AD": true means the answer was DNSSEC-validated by the resolver (1.1.1.1 FAQ). Over DoH the TLS session is with the resolver operator, so an on-path router cannot strip the bit the way it can on plain UDP/53. See dnssec for what the bit means.
For the wire-format standard (RFC 8484 application/dns-message), use kdig:
kdig @1.1.1.1 +https=/dns-query cloudflare.comnslookup cannot send queries over DoH. Use
curlorkdigas shown above.
Testing DoQ#
DoQ is the one transport BIND's dig still cannot do; dig @9.9.9.9 +quic example.com A returns Invalid option: +quic. Use kdig, which speaks DoQ when built with QUIC support (the Debian/Ubuntu package is):
kdig @9.9.9.9 +quic example.comThe first output line reads ;; QUIC session (QUICv1)-(TLS1.3)-... and the footer ;; From 9.9.9.9@853(QUIC), confirming the transport. Cloudflare publishes no DoQ endpoint for 1.1.1.1; its documented transports are DoT on 853 and DoH on 443, including DNS over HTTP/3, which is QUIC underneath but stays DoH on port 443 rather than DoQ on 853 (1.1.1.1 encryption docs). Google publishes no DoQ endpoint for 8.8.8.8 either.
Gotchas#
- Fail-open unless strict. RFC 8310's opportunistic profile falls back to plaintext port 53 if the DoT handshake fails; only the strict profile (RFC 8310 §5) treats a failed handshake as no DNS service. Check which profile your stub resolver runs before relying on DoT to hide anything.
- The destination is still visible. TLS SNI (without ECH, RFC 9849; see HTTPS/SVCB records), the TCP/UDP destination IP, and packet timing leak which site you're visiting even when the query itself is encrypted.
- The resolver operator sees every query. Encrypted DNS moves trust from the on-path network to whoever runs the resolver, not away from surveillance.
- Only the stub-to-recursive hop is encrypted. The recursive-to-authoritative hop stays plaintext unless the resolver runs Authoritative DNS over TLS, which is rare in production.
- The DoT hostname and DoH JSON path are not interchangeable. A certificate SAN matching the TLS name you configured proves you reached the operator (RFC 8310 §8.1). Google serves JSON from a separate
/resolvepath; sendingaccept: application/dns-jsontohttps://dns.google/dns-queryreturns HTTP 400. Quad9's/dns-queryspeaks only the RFC 8484 wire format and returns HTTP 400 for that same header. - The JSON format has no RFC. Cloudflare's own docs: "The DNS over HTTPS JSON format does not have a formal RFC, which means behavior might be different between providers" (Using JSON). Use the RFC 8484 wire format if you need a stable parse.
Public resolver support#
| Resolver | DoT :853 (TLS name) | DoH wire format | DoH JSON | DoQ :853 |
|---|---|---|---|---|
1.1.1.1 (Cloudflare) | one.one.one.one (doc) | https://cloudflare-dns.com/dns-query | /dns-query + accept: application/dns-json (doc) | no (doc) |
8.8.8.8 (Google) | dns.google (doc) | https://dns.google/dns-query (doc) | https://dns.google/resolve (doc) | no |
9.9.9.9 (Quad9) | dns.quad9.net (doc) | https://dns.quad9.net/dns-query | none | yes (blog, enabled 2026-03-31) |
94.140.14.14 (AdGuard DNS) | dns.adguard-dns.com (doc) | https://dns.adguard-dns.com/dns-query | none | yes |