feat(odoh): ship ODoH client + self-hosted relay (RFC 9230)
Client (mode = "odoh"): URL-query target routing per RFC 9230 §5,
/.well-known/odohconfigs TTL cache with 60s backoff on failure, HPKE
seal/open via odoh-rs, strict-mode default that SERVFAILs on relay
failure instead of silently downgrading. Host-equality config
validation rejects same-operator relay/target pairs.
Relay (`numa relay [PORT]`): axum server with /relay + /health.
SSRF-hardened hostname validator (RFC 1035 ASCII + dot + dash),
4 KiB body cap at the axum layer, 5s full-transaction timeout, and
static 502 on target failure (reqwest internals logged, not leaked).
Aggregate counters only — no per-request logs.
Observability: new `UpstreamTransport { Udp, Doh, Dot, Odoh }`
orthogonal to `QueryPath`, so /stats can tally wire protocols
symmetrically. Recursive mode records `Some(Udp)` for honest
"bytes egressing in cleartext" accounting.
Tests: Suite 8 exercises the client end-to-end via Frank Denis's
public relay + Cloudflare target; Suite 9 exercises `numa relay`
forwarding + guards against Cloudflare as the real far end. Full
probe script at tests/probe-odoh-ecosystem.sh verifies the entire
public ODoH ecosystem (4 targets + 1 relay per DNSCrypt's curated
list — confirms deploying Numa's relay doubles global supply).
This commit is contained in:
101
tests/probe-odoh-ecosystem.sh
Executable file
101
tests/probe-odoh-ecosystem.sh
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env bash
|
||||
# Probe the public ODoH ecosystem.
|
||||
#
|
||||
# Source of truth: DNSCrypt's curated list at
|
||||
# https://github.com/DNSCrypt/dnscrypt-resolvers/tree/master/v3
|
||||
# - v3/odoh-servers.md (ODoH targets)
|
||||
# - v3/odoh-relays.md (ODoH relays)
|
||||
#
|
||||
# As of commit 2025-09-16 ("odohrelay-crypto-sx seems to be the only ODoH
|
||||
# relay left"), the full public ecosystem is 4 targets + 1 relay. Re-run this
|
||||
# script against the upstream list before making any "only N public relays"
|
||||
# claim publicly.
|
||||
#
|
||||
# Usage: ./tests/probe-odoh-ecosystem.sh
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
GREEN="\033[32m"
|
||||
RED="\033[31m"
|
||||
YELLOW="\033[33m"
|
||||
DIM="\033[90m"
|
||||
RESET="\033[0m"
|
||||
|
||||
UP=0
|
||||
DOWN=0
|
||||
|
||||
probe_target() {
|
||||
local name="$1"
|
||||
local host="$2"
|
||||
local url="https://${host}/.well-known/odohconfigs"
|
||||
local start=$(date +%s%N)
|
||||
local headers
|
||||
headers=$(curl -sS -o /tmp/odoh-probe-body -D - --max-time 5 -A "numa-odoh-probe/0.1" "$url" 2>&1) || {
|
||||
DOWN=$((DOWN + 1))
|
||||
printf " ${RED}✗${RESET} %-25s ${DIM}unreachable${RESET}\n" "$name"
|
||||
return
|
||||
}
|
||||
local elapsed_ms=$((($(date +%s%N) - start) / 1000000))
|
||||
local status
|
||||
status=$(echo "$headers" | head -1 | awk '{print $2}')
|
||||
local ctype
|
||||
ctype=$(echo "$headers" | grep -i '^content-type:' | head -1 | tr -d '\r')
|
||||
local size
|
||||
size=$(stat -f%z /tmp/odoh-probe-body 2>/dev/null || stat -c%s /tmp/odoh-probe-body 2>/dev/null || echo 0)
|
||||
|
||||
if [[ "$status" == "200" ]] && [[ "$size" -gt 0 ]]; then
|
||||
UP=$((UP + 1))
|
||||
printf " ${GREEN}✓${RESET} %-25s ${DIM}%4dms %s bytes %s${RESET}\n" "$name" "$elapsed_ms" "$size" "$ctype"
|
||||
else
|
||||
DOWN=$((DOWN + 1))
|
||||
printf " ${RED}✗${RESET} %-25s ${DIM}status=%s size=%s${RESET}\n" "$name" "$status" "$size"
|
||||
fi
|
||||
rm -f /tmp/odoh-probe-body
|
||||
}
|
||||
|
||||
probe_relay() {
|
||||
# Relays don't expose /.well-known/odohconfigs — we just verify TLS reachability
|
||||
# and that the endpoint responds to a malformed POST with an HTTP error
|
||||
# (indicating the relay path exists). A real ODoH validation requires HPKE.
|
||||
local name="$1"
|
||||
local url="$2"
|
||||
local start=$(date +%s%N)
|
||||
local status
|
||||
status=$(curl -sS -o /dev/null -w "%{http_code}" --max-time 5 -A "numa-odoh-probe/0.1" \
|
||||
-X POST -H "Content-Type: application/oblivious-dns-message" \
|
||||
--data-binary "" "$url" 2>&1) || {
|
||||
DOWN=$((DOWN + 1))
|
||||
printf " ${RED}✗${RESET} %-25s ${DIM}unreachable${RESET}\n" "$name"
|
||||
return
|
||||
}
|
||||
local elapsed_ms=$((($(date +%s%N) - start) / 1000000))
|
||||
# Any 2xx or 4xx means the endpoint is live (TLS works, HTTP responded).
|
||||
# 5xx or 000 (curl failure) means broken.
|
||||
if [[ "$status" =~ ^[24] ]]; then
|
||||
UP=$((UP + 1))
|
||||
printf " ${GREEN}✓${RESET} %-25s ${DIM}%4dms status=%s (endpoint live)${RESET}\n" "$name" "$elapsed_ms" "$status"
|
||||
else
|
||||
DOWN=$((DOWN + 1))
|
||||
printf " ${RED}✗${RESET} %-25s ${DIM}status=%s${RESET}\n" "$name" "$status"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "ODoH targets:"
|
||||
probe_target "Cloudflare" "odoh.cloudflare-dns.com"
|
||||
probe_target "crypto.sx" "odoh.crypto.sx"
|
||||
probe_target "Snowstorm" "dope.snowstorm.love"
|
||||
probe_target "Tiarap" "doh.tiarap.org"
|
||||
|
||||
echo
|
||||
echo "ODoH relays:"
|
||||
probe_relay "Frank Denis (Fastly)" "https://odoh-relay.edgecompute.app/proxy"
|
||||
|
||||
echo
|
||||
TOTAL=$((UP + DOWN))
|
||||
if [[ "$DOWN" -eq 0 ]]; then
|
||||
printf "${GREEN}All %d endpoints up${RESET}\n" "$TOTAL"
|
||||
exit 0
|
||||
else
|
||||
printf "${YELLOW}%d/%d up, %d down${RESET}\n" "$UP" "$TOTAL" "$DOWN"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user