DoT, DoH, DoQ: encrypted DNS
DNS over TLS (DoT, port 853), DNS over HTTPS (DoH, port 443), DNS over QUIC (DoQ, port 853). All three encrypt the wire. None of them encrypt the records: those are still public. What changes is who can see which queries you ask.
Encrypted DNS is orthogonal to dnssec: DNSSEC proves a record is authentic; DoT/DoH/DoQ prevent observers from seeing which names you asked for at all. See anatomy-of-a-query for where in the query path each layer applies.
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 and Google's 8.8.8.8 both serve DoT at port 853, and either works as a test target. kdig (from knot-dnsutils) is the easiest CLI for this because mainstream dig only added +tls in BIND 9.18 ("The dig tool is now able to send DoT queries (+tls option)"):
kdig @1.1.1.1 +tls cloudflare.comInstall kdig via apt install knot-dnsutils on Debian/Ubuntu or brew install knot on macOS.
Modern BIND dig does the job too if you only need DoT or DoH. Checked on DiG 9.20.24 2026-08-16, dig @1.1.1.1 +tls cloudflare.com A, dig @8.8.8.8 +tls example.com A, and dig @9.9.9.9 +tls example.com A all returned NOERROR with flags: qr rd ra ad, and dig @9.9.9.9 +https=/dns-query example.com A did the same. What dig will not give you is kdig's transport banner, which is why kdig stays the better tool for proving how the query left.
nslookup cannot send queries over DoT. Use
kdigfor DoT testing.
Stock macOS
dig(BIND 9.10.x) does NOT support+tlsor+https. Ifdig +tlsreturns an option error, yourdigpredates 9.18. Installkdiginstead.
The output looks like plain dig output 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). That footer is the proof the query really left over port 853 encrypted. To see the certificate the resolver presents on port 853:
openssl s_client -connect 1.1.1.1:853 -servername 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)The CN=cloudflare-dns.com confirms you are talking to Cloudflare's DoT service, not an interceptor. Verify return code: 0 (ok) means the certificate chain validates against your system trust store. Three caveats. The field spacing is an OpenSSL version artefact: OpenSSL 3 prints C=US, OpenSSL 1.1 prints C = US, and the LibreSSL that ships with macOS prints the whole thing slash-separated (subject=/C=US/ST=California/...). The issuer changes whenever Cloudflare rotates CAs. And Cloudflare's own DoT documentation names cloudflare-dns.com, one.one.one.one, and the 1.1.1.1 / 1.0.0.1 addresses as the endpoint set (DNS over TLS), so match on the subject CN and the verify code, never on the issuer.
The same command works against the other two, checked 2026-08-16:
openssl s_client -connect 8.8.8.8:853 -servername dns.google </dev/null 2>/dev/null \
| grep -E "subject=|issuer=|Verify"subject=CN=dns.google
issuer=C=US, O=Google Trust Services, CN=WE2
Verify return code: 0 (ok)openssl s_client -connect 9.9.9.9:853 -servername dns.quad9.net </dev/null 2>/dev/null \
| grep -E "subject=|issuer=|Verify"subject=C=CH, ST=Zurich, L=Zürich, O=Quad9, CN=dns.quad9.net
issuer=C=US, O=DigiCert Inc, CN=DigiCert Global G3 TLS ECC SHA384 2020 CA1
Verify return code: 0 (ok)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. Cloudflare states "1.1.1.1 is a DNSSEC-validating resolver. On every query, it sends the DO (DNSSEC OK) flag to signal that it can accept signed responses" (1.1.1.1 FAQ). The endpoint and header above are Cloudflare's own documented pair (DoH JSON API). See dnssec for what the bit means.
This is the query to reach for when the local network intercepts UDP/53: a plain dig to 1.1.1.1 from behind an intercepting router is answered by the router, which strips the AD bit. Over DoH the TLS session is with Cloudflare, so the bit is Cloudflare's.
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.
On Windows 11 and Windows Server 2022+, DoH is an OS-level setting, configured per network interface and only for resolvers on the system's known-DoH-server list (Get-DnsClientDohServerAddress shows the list, Add-DnsClientDohServerAddress extends it; see Microsoft's DoH client docs). There is no built-in one-off command that points a single query at an arbitrary DoH endpoint, so to test a specific DoH server from Windows use curl (bundled since Windows 10 1803) or kdig as shown above.
Testing DoQ#
DoQ is the one transport BIND's dig still cannot do. Checked on DiG 9.20.24 2026-08-16: +tls and +https both work, and 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. Quad9 enabled DoH3 and DoQ across its global resolver network on 2026-03-31; AdGuard DNS (94.140.14.14) and Control D also answer DoQ.
Cloudflare publishes no DoQ endpoint for 1.1.1.1. Its documented encrypted transports are DoT on 853 and DoH on 443, including DNS over HTTP/3, which is QUIC underneath but is still DoH on port 443 rather than DoQ on 853 (1.1.1.1 encryption docs, Android setup). Google publishes no DoQ endpoint for 8.8.8.8 either.
What encrypted DNS does not do#
- Records are still public. dnssec handles authenticity; encrypted DNS handles query privacy. Neither makes records confidential.
- Your destination IP is still visible to anyone on the wire. TLS SNI (until ECH is universally deployed, see HTTPS/SVCB records), TCP destination IP, and the size and timing of packets all leak information even when the query contents are encrypted.
- The resolver still sees every query you make. Encrypted DNS moves trust from the on-path network to the resolver operator.
- It only encrypts the stub-to-recursive hop. The recursive-to-authoritative hop is still plaintext UDP/TCP unless Authoritative DNS over TLS (ADoT) is in use, which is rare in production today.
So encrypted DNS protects you from your ISP, captive portals, and on-path observers seeing the names you look up. It does not hide the existence of the connection or its destination.
When to use which#
- DoH: the only one that blends with browser traffic on port 443. Works on corporate networks that allow web traffic but block port 853. Hardest to selectively block without breaking the web.
- DoT: the cleanest separation for stub-to-recursive on a server or appliance. Easier to inspect and monitor at the network layer if you control the network and want to allow encrypted DNS but log its existence.
- DoQ: newer, runs over QUIC (UDP/853), allows 0-RTT after the first session. Adoption is growing: Quad9 enabled it network-wide in 2026, AdGuard and Control D have deployments, but mainstream OS stub resolvers do not speak it yet.
Which public resolvers support encrypted DNS#
| Resolver | Plain :53 | DoT :853 (TLS name) | DoH wire format | DoH JSON | DoQ :853 |
|---|---|---|---|---|---|
1.1.1.1 (Cloudflare) | yes | yes (cloudflare-dns.com) | https://cloudflare-dns.com/dns-query | /dns-query + accept: application/dns-json | no |
8.8.8.8 (Google) | yes | yes (dns.google) | https://dns.google/dns-query | https://dns.google/resolve | no |
9.9.9.9 (Quad9) | yes | yes (dns.quad9.net) | https://dns.quad9.net/dns-query | none | yes |
Checked 2026-08-16: openssl s_client -connect <ip>:853 -servername <name> returned Verify return code: 0 (ok) against all three DoT names, and each DoH URL above answered as listed.
Two traps in that table. First, the DoT hostname is not the DoH hostname's job: the TLS certificate is what proves you reached the operator, the IP is only what the OS dials. Second, the JSON variant is not one shape. Cloudflare serves JSON from the same /dns-query path when you send accept: application/dns-json. Google serves JSON from a separate /resolve path; sending accept: application/dns-json to https://dns.google/dns-query returns HTTP 400. Quad9's /dns-query speaks only the RFC 8484 wire format and returns HTTP 400 for the JSON accept header. Cloudflare warns that "the DoH JSON format has no formal RFC and its schema is not guaranteed to be stable", and changed the data field encoding for several record types in a July 2026 rollout; use the wire format if you need a stable parse.
See also#
- anatomy-of-a-query
- dnssec for the orthogonal authenticity story
- HTTPS/SVCB records for ECH and the broader "what TLS still leaks" story
- nslookup-and-dig for the baseline tools