isitdns? wiki/Security/DoT, DoH, DoQ: encrypted DNS
Security

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 +tls
; <<>> DiG 9.20.11-0ubuntu0.2-Ubuntu <<>> @1.1.1.1 example.isitdns.net A +tls
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 31071
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; PADDING: (400 bytes)
;; QUESTION SECTION:
;example.isitdns.net. IN A
;; ANSWER SECTION:
example.isitdns.net. 300 IN A 192.0.2.1
;; Query time: 35 msec
;; SERVER: 1.1.1.1#853(1.1.1.1) (TLS)
;; WHEN: Mon Sep 14 17:44:03 CDT 2026
;; MSG SIZE rcvd: 468

GO NOERROR, and a SERVER line ending 853 and (TLS)

NO GO a connection failure, which means port 853 is blocked on your path

what each part of this output means →

The three protocols at a glance#

DoTDoHDoQ
RFC785884849250
Port853 (TCP)443853 (UDP)
TransportTCP + TLSHTTPSQUIC (UDP)
Hides on the wireyesyes (looks like HTTPS)yes
Blends with web trafficno (distinct port)yes (port 443)no (distinct port)
Per-query latencyTCP + TLS handshakeTCP + TLS + HTTP framing0-RTT possible
Connection reuseyesyesyes

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.com

Install 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 +tls or +https. An option error means your dig predates 9.18; install kdig.

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_client as 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.com

nslookup cannot send queries over DoH. Use curl or kdig as 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.com

The 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 /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 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#

ResolverDoT :853 (TLS name)DoH wire formatDoH JSONDoQ :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-querynoneyes (blog, enabled 2026-03-31)
94.140.14.14 (AdGuard DNS)dns.adguard-dns.com (doc)https://dns.adguard-dns.com/dns-querynoneyes

See also#