DNS-over-HTTPS
Plain UDP/53 is unencrypted, unauthenticated, and blocked outright by half the networks an agent runs on — CI sandboxes, corporate egress proxies, serverless containers with no raw sockets.
DoH turns a DNS query into an HTTPS request, so if an agent can reach the web at all, it can resolve through Whisper's policy-aware, graph-first resolver — every answer wrapped in TLS instead of sitting in the clear for anyone on the path to read or spoof.
That matters more for agents than it ever did for browsers. An agent spawned in a container often has exactly one open path out — port 443 to a fixed set of hosts — and no /etc/resolv.conf control worth trusting. DNS-over-HTTPS (DoH), specified in RFC 8484, lets that agent send a real DNS message over the same HTTPS stack it already uses for everything else, and get back a real DNS message, confidentiality- and integrity-protected end to end.
The wire format, precisely
RFC 8484 defines DoH as HTTP carrying the standard DNS message format from RFC 1035 — not a redesigned protocol, just DNS's existing 12-byte header + question + RR sections, transported over HTTPS instead of UDP/53. Two request shapes are defined:
POST — the query is the literal, unmodified DNS wire-format message, sent as the request body:
:method = POST
:scheme = https
:authority = doh.whisper.online
:path = /dns-query
accept = application/dns-message
content-type = application/dns-message
content-length = <length of the DNS message>
<the DNS message in wire format, verbatim>
GET — the same wire-format message, base64url-encoded (RFC 4648 §5, no padding) into a single dns query parameter:
:method = GET
:scheme = https
:authority = doh.whisper.online
:path = /dns-query?dns=AAABAAABAAAAAAAAA2FjZQVhZ2VudA...
accept = application/dns-message
GET is what makes DoH cacheable by ordinary HTTP infrastructure (the response's Cache-Control / TTL-derived max-age behaves exactly like any other HTTP resource) — a shared HTTP cache in front of a resolver can shave real load off hot names without any DNS-specific logic. POST has no such caching but avoids URL-length limits on huge messages (large DNSSEC RRsets, many-name queries). Both forms are answered with content-type: application/dns-message — a standard DNS response, RCODE and all, byte-identical to what you'd get on UDP/53 except it arrived over TLS 1.2+.
There is also a JSON form some resolvers offer (application/dns-json, non-standardized, originally a Google/Cloudflare convenience format) for curl-and-jq debugging without a hex editor. Whisper's resolver speaks the RFC 8484 wire format; use the wire form for anything that has to be correct, and reach for a JSON-speaking public resolver only for throwaway shell debugging.
Why the mechanism, not just the transport
DoH is not "DNS but encrypted" as an afterthought — it's DNS as an HTTP resource, which is what makes it survive networks that plain DNS can't cross:
- It rides port 443, the one port that is almost never firewalled, proxied-away, or MITM'd by a corporate TLS-inspecting proxy without the proxy's own CA being trusted client-side (which would break every other HTTPS call too, so it's rarely done to DoH specifically).
- The DNS message is opaque inside the TLS record — an on-path observer sees an HTTPS connection to
doh.whisper.online, not which names were queried.DNS0x20and QNAME case aren't visible to anyone but the two TLS endpoints. - Because it's a normal HTTP request, it carries normal HTTP headers — which is exactly the seam Whisper uses to attach a per-tenant API key (
X-API-Key) to a DNS query. Plain UDP/53 has no header field for that; DoH does.
The Whisper resolver over DoH
Point an agent's DoH client at https://doh.whisper.online/dns-query with its X-API-Key, and every query runs through the same graph-first policy resolution whisper-ns applies on :53: the name is checked against the live security graph — geography, category, ownership, listing status — before an answer is returned; a graph opinion drives the response, and a clean name falls through to upstream DNS unmodified. Fail-open, always — if the graph is unreachable, resolution proceeds as plain, correct DNS rather than hanging or erroring, per the robustness principle this whole system is built on.
DoH is one of two ways to reach that same policy resolver — pick per your network:
| Transport | Auth | Best for | |
|---|---|---|---|
| DoH | HTTPS/443, application/dns-message |
X-API-Key header |
egress-restricted networks, browsers, anything that can already do HTTPS |
Dedicated :53 |
plain UDP/TCP 53 | the resolver IP itself (a per-tenant /128) |
agents with open UDP egress that want zero per-query overhead |
Both are Tier 2 in the connectivity tiers — "keep your own address, use Whisper for policy + control" — as opposed to Tier 1/1.5, where the agent's traffic actually egresses from a routable Whisper /128.
With stock tools
No Whisper software required — any HTTP client that can build a DNS message works. Because the Whisper resolver authenticates every DoH request — it is never an open resolver — each example carries your tenant token. The token is polymorphic in how it's presented; pick whichever channel your client can do:
X-API-Key: <token>header (orAuthorization: Bearer <token>) — the default for anything that sets headers.- path-embedded —
https://doh.whisper.online/<token>/dns-query— for clients (likekdig) that can't add a header. - host subdomain —
<token>.doh.whisper.online— a per-tenant hostname.
Presenting the same token on two channels is fine; presenting two different tokens is rejected outright (never silently resolved with one).
kdig (from knot-dnsutils) speaks DoH natively; give it the token in the path, since kdig sets no custom HTTP header:
# kdig native DoH (+https=<path>) — token rides the URL path
$ kdig +https=/whisper_live_.../dns-query @doh.whisper.online \
acef2002a323d40d4.demo.agents.whisper.online AAAA
;; DoH session (HTTP/2 https://doh.whisper.online:443/whisper_live_.../dns-query)
;; ANSWER SECTION:
acef2002a323d40d4.demo.agents.whisper.online. 300 IN AAAA 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4
Or build the raw wire-format message yourself and POST it with curl — the fully manual path, useful for verifying exactly what's on the wire. You don't even need a DNS library: a plain AAAA query is just the 12-byte header + one question, so a few lines of python3 stdlib hand-assemble it:
# Hand-build a minimal wire-format AAAA query (id, flags, 1 question, no EDNS)
$ python3 - <<'EOF' > query.bin
import struct
name = "acef2002a323d40d4.demo.agents.whisper.online"
qname = b"".join(struct.pack("B", len(l)) + l.encode() for l in name.split(".")) + b"\x00"
header = struct.pack(">HHHHHH", 0x1234, 0x0100, 1, 0, 0, 0) # id, RD=1, QDCOUNT=1
question = qname + struct.pack(">HH", 28, 1) # QTYPE=AAAA(28), QCLASS=IN(1)
import sys; sys.stdout.buffer.write(header + question)
EOF
$ curl -s -X POST https://doh.whisper.online/dns-query \
-H 'content-type: application/dns-message' \
-H 'accept: application/dns-message' \
-H 'X-API-Key: whisper_live_...' \
--data-binary @query.bin -o answer.bin
$ xxd answer.bin | head -4 # RCODE is the low nibble of byte 3; ANCOUNT is bytes 6-7
GET form, base64url-encoding the same query with plain shell tools — here presenting the token in the path instead of a header (both authenticate identically):
$ QUERY_B64=$(base64 -w0 query.bin | tr '+/' '-_' | tr -d '=')
$ curl -s "https://doh.whisper.online/whisper_live_.../dns-query?dns=${QUERY_B64}" \
-H 'accept: application/dns-message' -o answer.bin
With Whisper
The whisper CLI provisions an agent and hands you a key already scoped to the resolver — one command, the key shown once:
$ whisper create --register --name my-agent
whisper: created my-agent — 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4
api_key: whisper_live_... # shown once — this is your DoH credential
Under the hood that's a single control-plane call — graph.whisper.security is the same Cypher-shaped control API the CLI itself speaks:
CALL whisper.agents({op:'register', args:{label:'my-agent'}})
-> { api_key: "whisper_live_...",
agent: "agent-acef2002a323d40d4",
address: "2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4",
fqdn: "acef2002a323d40d4.demo.agents.whisper.online",
ptr: "acef2002a323d40d4.demo.agents.whisper.online" }
The DoH endpoint itself is a single constant — https://doh.whisper.online/dns-query — so there is nothing to allocate per agent: present the returned api_key (header, path, or host-subdomain, above) to any HTTPS-capable DoH client and you're resolving through the policy engine:
$ curl -s "https://doh.whisper.online/dns-query?dns=${QUERY_B64}" \
-H 'accept: application/dns-message' \
-H 'X-API-Key: whisper_live_...' -o answer.bin
No allowlisting a new IP, no opening UDP/53, because it's a hostname on 443 your egress already trusts. See Connect & egress for the full tier comparison and the WireGuard/SOCKS5 tiers that go further than "just resolve" into "actually route as the agent's own identity."
Next
Read Graph-first resolution for what happens after the DoH message lands — the policy engine that decides an answer before falling through to upstream DNS — and Connect & egress for how DoH fits alongside WireGuard and SOCKS5 as one of the four ways an agent reaches the Whisper network.