Graph-first resolution
A stock recursive resolver answers every query it can — which is exactly the problem.
It has no opinion about a name registered six minutes ago, hosted on bulletproof infrastructure, behind a hijacked prefix, or owned by an entity you've explicitly banned. It resolves that just as happily as it resolves github.com, and your agent connects.
Static blocklists chase that gap and always lose — a feed refreshed hourly is still hours behind a fast-flux C2 domain or a newly announced /24 that just hijacked someone else's routes. Whisper's resolver closes the gap a different way: it doesn't consult a list, it asks a live graph — the same one behind RDAP and whisper.agents — a question with real teeth: who owns this, where does it sit, what category is it, is its route valid right now? — before it lets a single query leave for the wider internet.
Two resolvers, same wire protocol
Nothing about the wire format changes. dig, kdig, and every stub resolver on earth still send the same RFC 1035 query and expect the same RFC 1035 response — a 12-byte header, a question section, and (on success) an answer section of resource records. The difference is what happens between the query landing and the answer leaving.
With stock tools, against a normal resolver — it forwards blindly, no matter what's on the other end:
$ dig @1.1.1.1 +noall +comments +answer AAAA some-freshly-registered-domain.example
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 41232
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
some-freshly-registered-domain.example. 300 IN AAAA 2606:4700::...
1.1.1.1 has no idea the name was registered nine minutes ago, or that its authoritative nameserver sits behind an ASN with a week-old RPKI-invalid announcement. It isn't supposed to know — that's not its job. It just does RFC 1035.
With Whisper, against an agent's resolver — same query, same wire format, but the answer reflects a policy decision made milliseconds earlier:
$ dig @2a04:2a01:0:53::53 +dnssec +noall +comments AAAA some-freshly-registered-domain.example
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 41232
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
Same source address, same query — but this tenant's policy blocks newly-registered as a category, so the name never reaches a real answer. No packet toward that domain's infrastructure ever left the agent's /128. The block is logged the same instant, visible via whisper.agents({op:'logs'}) or the CLI, with the category and rule that fired.
The flow: graph first, real DNS second
Every query an agent sends hits three possible outcomes, evaluated against your policy before resolution proceeds:
| Verdict | What happens | Example trigger |
|---|---|---|
| Allow | Falls through to real resolution — the graph has an opinion and it's positive, or you've explicitly allow-listed the owner/category | An entity you've named on your allow-list |
| Block | Answered on the spot — NXDOMAIN or a fixed sinkhole address, your choice via policy, logged with the rule that matched | Tor exit, bulletproof hosting, newly-registered domain (<48h old), a named owner, RPKI-invalid route |
| No opinion | Steps aside immediately — real upstream resolution proceeds at full speed, exactly like a stock resolver | The overwhelming majority of the internet: nothing in your policy or the graph's threat surface says otherwise |
The graph read carries a strict time budget. If it doesn't answer inside that budget, the resolver treats it as no opinion and fails open — a slow or unreachable policy check never becomes a resolution outage. This is deliberate: a control that can black-hole your production traffic when it has a bad day is worse than no control at all. Reliability wins; the query resolves, and the slow check is what gets flagged, not the agent.
Policy dimensions no static list can express
A blocklist is a flat set of names. The graph carries structure — ownership graphs, ASN and prefix delegation, RIR geolocation, real-time RPKI validity, and content/category classification — so a single policy rule can reach conclusions a list never could:
- Category — block a whole class of infrastructure: Tor exits, bulletproof hosting, newly-registered domains, known scanning ranges — not by enumerating IPs, but by the graph's classification of what a host is.
- Geography — geo-fence resolution to jurisdictions you've named, sourced from RFC 9092 geofeed data and RIR delegation, not GeoIP guesswork.
- Ownership — deny an entity, not an address. The same organization behind three different hosting providers gets caught once, by the ownership edge in the graph, not three separate IP-range rules.
- Routing — drop a destination the instant its announcing prefix goes RPKI-invalid (RFC 6811 origin validation) — a route hijack turns into an automatic block, live, without you ever touching a list.
Each dimension is one toggle on the control plane, not a maintenance job.
Setting policy: dual paths, same effect
With stock tools there is no equivalent — this is the point. A resolver that only speaks RFC 1035 has no channel for "block this category" short of you maintaining and pushing an RPZ zone or a blocklist file yourself, by hand, forever behind the threat.
With Whisper, one call, either as the raw control verb or the CLI wrapper:
# raw control plane — the whisper.agents verb, over the graph gateway, with your key
curl -s https://graph.whisper.security/api/query \
-H "X-API-Key: whisper_live_..." -H 'content-type: application/json' \
-d '{"query":"CALL whisper.agents({op:\"policy\", block:[\"tor-exit\",\"newly-registered\"], geo:{allow:\"EU\"}, routing:\"drop-hijacked\"})"}'
# the CLI, same effect
whisper policy my-agent --block tor-exit,newly-registered --geo-allow EU --routing drop-hijacked
From that point forward, every query from my-agent's address is measured against those rules before it resolves — verify it live:
dig @<agent's :53 resolver> +short AAAA <a known .onion-adjacent or freshly-registered test name>
# -> NXDOMAIN, and the block shows up in `whisper.agents({op:'logs'})` immediately
Two access paths, one policy engine
The graph-first check applies identically whether an agent reaches Whisper over DNS-over-HTTPS (RFC 8484, :443, API-key header — good for HTTP-only egress environments) or a dedicated per-tenant IPv6 /128 resolver on standard :53, where the source address of the query is the authentication — no key on the wire at all:
# DoH — keyed, wire-format DNS-over-HTTP
curl -s -H "X-API-Key: whisper_live_..." -H 'content-type: application/dns-message' \
--data-binary @query.bin https://doh.whisper.online/dns-query
# per-tenant :53 — keyless, the source /128 is the identity
kdig @2a04:2a01:0:53::53 +dnssec example.com AAAA
Both paths run the same three-way verdict, the same fail-open timeout, and the same per-query log line — pick the transport that fits your agent's egress path, not the policy.
Next
- Egress governance — the same graph-first check applied to outbound connections, not just names
- DNS-over-HTTPS — the keyed
:443transport for HTTP-only environments