WhisperDocs
Standards

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:

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:

Presenting the same token on two channels is fine; presenting two different tokens is rejected outright (never silently resolved with one) - verified live: a request with a valid header token and a different, bogus path token gets a clean 400, not a guess at which one to honor. There is no host-subdomain form (<token>.doh.whisper.online): a live API key is 72 characters, and a DNS label caps at 63 octets (RFC 1035 §3.1) - the hostname would not even be constructible for any key issued today.

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. (kdig's own +https client speaks HTTP/2 only, and this endpoint currently negotiates HTTP/1.1 - kdig +https=... against it fails at the TLS layer with no ALPN agreed. Reach for curl or another DoH client that falls back to HTTP/1.1 until that changes.)

# Hand-build a minimal wire-format AAAA query (id, flags, 1 question, no EDNS)
$ python3 - <<'EOF' > query.bin
import struct
name = "acef2002a323d40d4.t7af9242c44554e0f8e183ac8e6fbf645.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: identity ready
  agent    agent-a23b6d7fee65859c5
  address  2a04:2a01:97c8:cebd:23b6:d7fe:e658:59c5
  name     a23b6d7fee65859c5.t51c1088963b0263cd428e9c72da4e82c.agents.whisper.online
  ptr      5.c.9.5.8.5.6.e.e.f.7.d.6.b.3.2.d.b.e.c.8.c.7.9.1.0.a.2.4.0.a.2.ip6.arpa
  API KEY (shown once - store it now):
whisper_live_...

Under the hood that's a single control-plane call - graph.whisper.online is the same Cypher-shaped control API the CLI itself speaks, and it hands back the DoH URL ready-made:

CALL whisper.agents({op:'register', args:{label:'my-agent'}})
-> { api_key:     "whisper_live_...",
     agent:       "agent-ae3936fc69920c2d6",
     address:     "2a04:2a01:f372:9965:e393:6fc6:9920:c2d6",
     fqdn:        "ae3936fc69920c2d6.taaa5c23b2b31237d063514228cf34993.agents.whisper.online",
     ptr:         "6.d.2.c.0.2.9.9.6.c.f.6.3.9.3.e.5.6.9.9.2.7.3.f.1.0.a.2.4.0.a.2.ip6.arpa",
     doh_url:     "https://doh.whisper.online/whisper_live_.../dns-query",
     resolver_ip: "2a04:2a01:0:53:fea5:4045:734c:f0a" }

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 or path, above) to any HTTPS-capable DoH client and you're resolving through the policy engine, or just use the doh_url the register call already handed you:

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