HNS.ONE/ Oblivious DNS

HNS.ONE / Oblivious DNS

We cannot tell anyone what you looked up

The bottom line: odoh.hns.one is a live, public Oblivious DoH target (RFC 9230) that answers for Handshake and ordinary ICANN names. When you reach it through a relay — which is the only way we support — your address never arrives here. Not "we choose not to log it": it is not in the request. We cannot hand over, leak, or be compelled to produce a thing we never receive. Anyone may point a client at it, today, for free — you do not need an account, and you do not need anything of ours but the target address below.

Said exactly, because the boundary is the point.

What we see: the question, and the relay's address. What we never see: yours. So we can say what was asked today; we cannot say who asked it, and no amount of pressure on us changes that — we would have to be given data we do not hold.

Where that stops being true: if you point a client straight at this target with no relay, it is ordinary DoH and we see your address like any resolver. And a relay that colludes with us can put the two halves back together — it holds the address we lack. That is why the relay must be run by someone with no relationship to us, and why we deliberately run no relay at all.

Using it from your own software

This is a public service, not an HNS.ONE feature. Any RFC 9230 client can use it — a browser, a resolver, a script, another provider's stack — by setting two things:

Targetodoh.hns.one, path /dns-query — that is us
Relayanyone else's. Today that means odoh-relay.numa.rs. Never ours; we do not have one

That is the whole integration. You get Handshake names, which no other public ODoH target answers for, and ICANN names on the same endpoint — so a client does not need a second resolver for the rest of the internet. Verified working the day this page was written: 14898 (Handshake) and example.com both resolved through the numa relay to this target. The exact calls →

ODoH is experimental, and the relay pool is thin.

RFC 9230 is an experimental standard, and there are two public relays on the community list worldwide — one of which will not carry traffic to us until we are on the DNSCrypt list. That is the honest weak point: this depends on a small number of volunteers who are not us.

How to use it Pick a relay What it does not protect

What the split actually buys you

With ordinary DNS-over-HTTPS, one server learns both halves of the sentence: your IP address and every name you look up. Encryption hides that from your ISP; it does not hide it from the resolver. For a namespace like Handshake, where a handful of public resolvers serve nearly everybody, that is a very short list of parties who could assemble a complete browsing history.

Oblivious DoH splits the two halves between parties that each only get one:

  you ──sealed query──▶  RELAY  ──▶  odoh.hns.one  ──▶  hsd recursive
                     (someone else)     (us)         (the Handshake root zone)

  the relay sees   your address  +  an opaque blob it cannot open
  the target sees  the question  +  the relay's address, never yours

The query is sealed to the target's public key with HPKE before it leaves your machine, so the relay carries ciphertext. The target can open it, but the only client address it ever sees is the relay's. Neither one alone can say who asked what.

A relay and a target that collude can link you.

Nothing in the protocol prevents it. The relay knows your address, the target knows the question, and if the same operator — or two cooperating ones — hold both ends, you are exactly as exposed as with plain DoH. This is why the relay must be run by someone with no relationship to us, and why we do not run one.

The target

Endpointhttps://odoh.hns.one/dns-query
Key confighttps://odoh.hns.one/.well-known/odohconfigs
Healthhttps://odoh.hns.one/health — returns ok
HPKE suiteDHKEM(X25519, HKDF-SHA256) / HKDF-SHA256 / AES-128-GCM
Media typeapplication/oblivious-dns-message
UpstreamA Handshake-aware hsd recursive resolver on the same host — the same resolver behind our plain-DoH endpoint query.hns.one. It answers both Handshake and ICANN names.
Relay on this hostDisabled. /proxy returns 404 so this box is incapable of relaying to its own target.
TLSPublicly trusted certificate, plus a DANE pin (TLSA 3 1 1 at _443._tcp.odoh.hns.one) for clients that check it.
LoggingThe vhost runs with nginx access logging off. Clients arrive through a relay in any case, so the address a log could hold is the relay's.

The target is Cloudflare's odoh-server-go (MIT) with one change: the upstream nameservers are environment-configurable, so it can forward to a Handshake-aware recursive instead of an ICANN-only one. That is the entire trick. Anyone can run one.

Using it

1. Check it is alive

curl -s https://odoh.hns.one/health
# ok

2. Fetch the key config

The config is public, fetched straight from the target over TLS — going through a relay to get it would add nothing, since a tampered config only makes your own decryption fail.

curl -s https://odoh.hns.one/.well-known/odohconfigs | xxd
# 00000000: 002c 0001 0028 0020 0001 0001 0020 <32-byte X25519 public key>

Reading it: a 2-byte list length, then one config — version 0x0001, length, then the contents: KEM 0x0020 (X25519-HKDF-SHA256), KDF 0x0001 (HKDF-SHA256), AEAD 0x0001 (AES-128-GCM), and the public key. The key id a client puts in each message is Expand(Extract("", contents), "odoh key id", 32) over those contents verbatim — RFC 9230 §6.1.

3. Send a query

curl cannot do this part. The query has to be HPKE-sealed before it is sent, so you need an RFC 9230 client. What such a client does is short: seal the DNS wire query to the config's public key, then POST it to your chosen relay with the target named in the query string —

POST https://<relay>/<path>?targethost=odoh.hns.one&targetpath=%2Fdns-query
content-type: application/oblivious-dns-message
accept: application/oblivious-dns-message
body: the sealed ObliviousDoHMessage

The response comes back sealed too, under keys derived from the HPKE exporter secret and a nonce the target chooses, so it never rides the HPKE context directly.

Our own client — the one Wildroot ships — is a dependency-light ES module built on @hpke/core and @hpke/dhkem-x25519, MIT licensed. A query through it looks like this:

const t = new OdohTransport({
  target: 'odoh.hns.one',
  relays: ['https://odoh-relay.numa.rs/relay']
})
const { answer, via } = await t.query(dnsWireQuery)   // Uint8Array in, out

Measured in August 2026, from a home connection through the numa relay: 972 ms for the first lookup including the config fetch and HPKE setup, 214 ms for a warm one. Going straight to the target with no relay took about 1.3 s cold — and is not oblivious: that is just DoH, and we would see your address.

The target has no DNS stamp on the DNSCrypt public list yet, so dnscrypt-proxy users cannot add it by name today. That submission is on our list; until it lands, clients need it configured by hand.

You have to bring your own relay

We run a target and nothing else. There is no hns.one relay to pair with it, by design and by configuration — /proxy on the target host returns 404, so even a misconfigured client cannot accidentally send both halves of its identity to the same machine.

RelayWorks with our target?
https://odoh-relay.numa.rs/relayworks Run by the Numa project (Hetzner, Falkenstein). Verified carrying our traffic in August 2026. This is the one to use today.
https://odoh-relay.edgecompute.app/not yet The crypto.sx relay, on Fastly. It only relays to targets on the DNSCrypt public ODoH list, and we are not on that list yet, so queries to us through it fail. It is worth configuring anyway — it starts working the day the listing lands, with no client change.
Your ownbest A relay is the easy half to operate and the ecosystem needs more of them than it needs targets. Run one on a host and an organization that has nothing to do with any target its users query.

Two relays is a thin pool, and it is the honest weak point of ODoH today: a small relay set is itself a correlation surface, and a relay outage is something you will notice.

The signed feed

Picking a target and relay that are genuinely independent is fiddly to do by hand, so we publish the answer as a small JSON document:

https://hns.one/.well-known/odoh-feed.json

It carries the community's ODoH targets and relays with operator identity attached, our own Handshake-aware target injected, and a precomputed pairs list of combinations that already satisfy the two rules a client must not get wrong: the relay's operator is never the target's operator, and the target advertises DNSSEC. Each pair also carries a handshake_aware flag — a Handshake lookup is only valid against a target that has it, and today exactly one target does. It is served with Access-Control-Allow-Origin: * and regenerated roughly every six hours.

curl -s https://hns.one/.well-known/odoh-feed.json | python3 -c '
import json,sys; f=json.load(sys.stdin)
print(len(f["targets"]), "targets,", len(f["relays"]), "relays,", len(f["pairs"]), "pairs")
print([t["host"] for t in f["targets"] if t["handshake_aware"]])'
# 133 targets, 2 relays, 265 pairs
# ['odoh.hns.one']

What the signature covers — and what it does not

The upstream lists come from the DNSCrypt community as signed files. Before the feed is written, their minisign (Ed25519) signatures are verified against the well-known DNSCrypt public key RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3, and the trusted comments from those signatures are copied into the feed's signature block. If verification fails, the generator exits and writes nothing — a stale feed rather than an unverified one. That matters because an attacker who could edit the relay list could hand every user a relay they control, which is the whole ballgame.

Wildroot does not read this feed yet — it ships the target and the relay list in its own configuration. The feed exists so that any client can pick a safe pair, and so that list can eventually change without shipping a browser release.

The feed itself is not signed by us.

signature.verified means we checked the upstream lists before emitting this file. It says nothing about the file you just downloaded: that is authenticated by TLS to hns.one and by nothing else. Consume the feed only from a host you already trust to have run that check — or read the lists and verify the signatures yourself.

Why a Handshake target is the point

Oblivious DoH has existed since 2020 and there are public targets for ordinary ICANN names. None of them can answer for Handshake: we surveyed eight public ODoH targets in August 2026, Cloudflare's included, and every one returned NXDOMAIN for Handshake names. Sending yourname/ to them does not protect the lookup, it breaks it.

So until this target existed, resolving Handshake privately meant running a full node — and everyone who did not run one was resolving through one of a small number of public Handshake resolvers that saw both who they were and every name they visited. A namespace with few resolvers is a namespace where each resolver sees an unusually complete picture. That is the specific hole this fills.

The right answer, where you can afford it, is still your own node: it is the only one that needs no trust at all. ODoH is for everybody else, and for the moments when the node is not there.

In Wildroot

Wildroot, our browser, uses this target by default (2.28 and later) — nothing to turn on. Its first choice is not DNS at all: when its bundled Handshake node is running and synced, names are resolved against the chain with a proof. Every lookup that does not take that path — while the node is starting or catching up, on machines where it cannot run, and deliberately whenever you switch on the browser's anonymization mode — goes out obliviously through this target instead of to a plain resolver. If the relay or the target cannot be reached, it falls back to plain DoH so that browsing does not break.

What the fallback costs you.

A fallback lookup goes to an ordinary DoH resolver (query.hns.one, hnsdoh.com, dns.easyhns.com) over TLS. That resolver sees your address and the name together, exactly like any other DoH — the oblivious property is gone for that lookup. The first of those three is ours, so on fallback we are usually the ones who can see both halves. The browser logs the fallback and says why, rather than degrading silently — but there is no switch today that makes a lookup fail instead of falling back. If you need that guarantee, run a node: chain resolution is the path that asks no resolver anything.

One detail worth knowing, because it is where a naive implementation leaks: an empty NOERROR answer is weak evidence and needs confirming. Confirming it over plain DoH would leak the name for every ordinary site, so Wildroot confirms it with a second oblivious query instead, and only reaches for plain DoH if that confirmation cannot be delivered at all.

ICANN names are a separate story. Wildroot sends those through Chromium's own encrypted DNS, which is not oblivious — Chromium accepts only https DoH templates today, so an ODoH bridge for them is not yet possible. Encrypted, not oblivious. We would rather say so than let the padlock imply otherwise.

Being straight with you

Get Wildroot Get a free Handshake name