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:
| Target | odoh.hns.one, path
/dns-query — that is us |
|---|---|
| Relay | anyone 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 →
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.
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.
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
| Endpoint | https://odoh.hns.one/dns-query |
|---|---|
| Key config | https://odoh.hns.one/.well-known/odohconfigs |
| Health | https://odoh.hns.one/health — returns ok |
| HPKE suite | DHKEM(X25519, HKDF-SHA256) / HKDF-SHA256 / AES-128-GCM |
| Media type | application/oblivious-dns-message |
| Upstream | A 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 host | Disabled. /proxy returns 404
so this box is incapable of relaying to its own target. |
| TLS | Publicly trusted certificate, plus a DANE pin
(TLSA 3 1 1 at _443._tcp.odoh.hns.one) for clients that check it. |
| Logging | The 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.
| Relay | Works 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.
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.
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
- A colluding relay and target defeat it completely. The guarantee is "no single party links you to your lookups", and it holds only while the two parties are genuinely separate. Choose a relay run by someone with nothing to do with us.
- You must supply that relay yourself. We run no relay, and one of the two public relays does not carry our traffic yet. Today that leaves one working public option.
- The relay learns that you use Handshake. It cannot
read your queries — that is the point — but the target's name travels in
the clear as
?targethost=odoh.hns.one, because the relay has to know where to forward. Today exactly one public ODoH target answers Handshake names, so a relay can tell that your address resolves Handshake at all, just never which names. With a user base this small, that fact is itself a signal. It shrinks as more Handshake-aware targets exist, which is one more reason we want company. - This is not a VPN. It protects the lookup. The page load that follows still comes from your address, straight to the site.
- This is not chain proof. A malicious target can lie about records. Oblivious resolution hides who is asking; it does not prove the answer. Verifying answers against the chain — a local node, plus DANE for the certificate — is a different and stronger property, and the one Wildroot uses where it can.
- You are trusting our configuration. "No query logs" is a claim about how we run the box, not something you can verify from outside. What you can verify from outside is that we cannot also be your relay.
- It costs a hop. Expect a couple of hundred milliseconds over plain DoH on a warm connection, more when a relay is having a bad day.
- The upstream resolver is the ceiling. Names whose own nameservers are broken, or that fail validation, return SERVFAIL or time out here — identically to how they fail over plain DoH. Oblivious does not mean better answers, only more private ones.
- This is early. One target, one region, a small operation, and no uptime promise.