feat(odoh): ship ODoH client + self-hosted relay (RFC 9230) #121
Reference in New Issue
Block a user
Delete Branch "feat/odoh"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
mode = "odoh"in[upstream]. HPKE seal/open viaodoh-rs, URL-query target routing per RFC 9230 §5, strict-mode SERVFAIL on relay failure (no silent downgrade),/.well-known/odohconfigsfetcher with TTL cache and 60s backoff.numa relay [PORT]subcommand. Forwards opaque POST bodies between client and target without reading them; SSRF-hardened, 4 KiB body cap, counters-only observability.QueryPath, symmetric counter for UDP/DoH/DoT/ODoH via/stats.upstream_transport. Lets the dashboard answer "how much of my DNS traffic left in cleartext?" honestly.What's in the tree
Client side (resolver talks ODoH upstream):
src/odoh.rs— fetcher,OdohConfigCache(lock-freeArcSwapOptionread path, single-flight refresh mutex, refresh backoff),query_through_relaywith retry-once on key rotation.src/forward.rs—Upstream::Odohvariant;Upstream::transport();build_https_client_with_pool(n)helper (pool=1 for DoH/ODoH client, pool=4 for relay's many-target fan-out).src/config.rs—UpstreamMode::Odoh,OdohUpstreamvalidator. Same-host rejection, https-only,fallbackfield accepts string-or-array.src/serve.rs— new match arm constructing the pool undermode = "odoh".Relay side (runs on its own host):
src/relay.rs— axum server, RFC 1035 hostname validator,DefaultBodyLimitlayer, static 502 body (internals logged not leaked),/healthwith aggregate counters.src/main.rs—numa relay [PORT]subcommand (default 8443).Observability:
src/stats.rs—UpstreamTransportenum, counters,StatsSnapshotfields.src/api.rs—/stats.upstream_transportJSON object mirroring client-sidetransport.src/ctx.rs— tags each successful resolution withSome(UpstreamTransport::*)orNone(cache/local/blocked).Tests:
tests/integration.shSuite 8 — ODoH client against Frank Denis's public relay + Cloudflare target. Covers strict-mode SERVFAIL and same-host config rejection.tests/integration.shSuite 9 —numa relayforwarding to Cloudflare + guards (missing content-type → 415, oversized body → 413, invalid targethost → 400).tests/probe-odoh-ecosystem.sh— probes the full ecosystem (4 targets + 1 relay) per DNSCrypt's curatedv3/odoh-relays.md.Ecosystem provenance
DNSCrypt's canonical list (
DNSCrypt/dnscrypt-resolvers/v3/odoh-relays.md, pruned 2025-09-16 with the commit message "odohrelay-crypto-sx seems to be the only ODoH relay left") currently has one public ODoH relay. Deploying Numa's relay doubles global supply, not "adds to a crowded field."Test plan
cargo test --lib— 339 passedcargo clippy --lib -- -D warnings— cleancargo fmt --check— cleancargo audit— only allowed warningsdig @127.0.0.1 example.comthrough ODoH resolver → NOERROR in ~146ms via Frank Denis's relay → Cloudflarenuma relayto Hetzner CX22; confirm external reachability