# DoH Forwarder — Project Plan ## Overview A lightweight dual-protocol DNS forwarder written in Rust. It accepts DNS queries over both traditional DNS (UDP/TCP) and DNS-over-HTTPS (RFC 8484), then forwards them upstream via DoH (HTTPS POST) to configurable resolvers. ## Goals - Accept DoH queries over HTTP (GET and POST per RFC 8484) - Accept traditional DNS queries over UDP and TCP - Forward all queries to upstream DoH resolvers via HTTPS POST - Support multiple upstream resolvers with round-robin or failover selection - TOML configuration with environment variable overrides - Graceful shutdown on SIGINT/SIGTERM - Health-check endpoint - Low resource footprint, single binary ## Architecture ```mermaid flowchart LR subgraph clients["Clients"] browser["Browser / App"] os["OS DNS Client"] end subgraph forwarder["DoH Forwarder"] doh["/dns-query (HTTP)"] health["/health"] dns["DNS Listener (UDP/TCP)"] upstream["Upstream Client"] end subgraph upstreams["Upstream DoH Resolvers"] cf["Cloudflare (1.1.1.1)"] google["Google (8.8.8.8)"] other["..."] end browser -- "HTTPS (RFC 8484)" --> doh os -- "UDP/TCP :5353" --> dns doh --> upstream dns --> upstream upstream -- "HTTPS POST" --> cf upstream -- "HTTPS POST" --> google upstream -- "HTTPS POST" --> other cf -- "DNS response" --> upstream google -- "DNS response" --> upstream other -- "DNS response" --> upstream ``` ## Tech Stack - **Language**: Rust - **HTTP server**: Axum - **DNS wire-format**: hickory-proto - **HTTP client**: reqwest (with rustls-tls) - **Async runtime**: Tokio - **Config**: TOML (config file) + env vars - **Logging**: tracing + tracing-subscriber (env-filter) - **Error handling**: thiserror, anyhow - **Encoding**: base64 ## Milestones ### M1 — Core Forwarding ✅ - [x] Project scaffold (Cargo.toml, src layout) - [x] HTTP server with `/dns-query` endpoint (GET + POST per RFC 8484) - [x] Parse incoming DNS wire-format queries (hickory-proto) - [x] Forward to upstream DoH resolver via HTTPS POST - [x] Return DNS wire-format response - [x] Health endpoint (`GET /health`) - [x] SERVFAIL response when upstream fails - [x] Integration tests (tests/integration.rs) ### M2 — Multiple Upstreams & Config ✅ - [x] TOML config file support (config/default.toml) - [x] Multiple upstream resolver support - [x] Upstream selection strategy (round-robin / failover) - [x] Automatic retry with next resolver on failure - [x] Environment variable overrides (DOH_LISTEN, DOH_UPSTREAM_STRATEGY, DOH_UPSTREAM_RESOLVERS) - [x] Graceful shutdown (SIGINT / SIGTERM) ### M3 — Local DNS Listener ✅ - [x] UDP listener on configurable port (default 5353) - [x] TCP listener on configurable port (default 5353) - [x] DNS over TCP with 2-byte length prefix (RFC 1035 §4.2.2) - [x] Forward through DoH pipeline (shared upstream client) - [x] Return DNS wire-format response over UDP/TCP - [x] Config: `[dns]` section with `listen` address - [ ] Integration test with UDP/TCP clients ### M4 — Caching - [ ] In-memory DNS response cache - [ ] Cache key = (query name, query type) - [ ] Respect DNS response TTL (with configurable max) - [ ] Cache eviction (TTL-based + LRU size cap) - [ ] Cache stats endpoint (`/cache/stats`) ### M5 — Observability - [ ] Request logging middleware (query name, type, latency, upstream used) - [ ] Metrics endpoint (Prometheus format) ### M6 — Blocking & Filtering - [ ] Domain blocklist (file-based, one domain per line) - [ ] Wildcard / suffix matching - [ ] Return NXDOMAIN for blocked queries - [ ] Hot-reload blocklist on SIGHUP ### M7 — Production Hardening - [ ] Rate limiting (per-IP) - [ ] Dockerfile (multi-stage, distroless) - [ ] Helm chart - [ ] CI pipeline (lint, test, build, image push) ### M8 — Censorship Resistance (the real differentiator) This is where the project goes beyond what `cloudflared` offers. See [story.md](story.md) for the full rationale. - [ ] SOCKS5 proxy support for upstream DoH connections - [ ] HTTP CONNECT proxy support for upstream DoH connections - [ ] Configurable upstream transport (direct / SOCKS5 / WireGuard tunnel) - [ ] Upstream health probing (latency + availability) - [ ] Auto-switch away from unreachable resolvers ## Directory Structure ``` doh-forwarder/ ├── Cargo.toml ├── config/ │ └── default.toml ├── src/ │ ├── main.rs # entrypoint, server setup, graceful shutdown │ ├── lib.rs # public API re-exports │ ├── config.rs # TOML config parsing + env var overrides │ ├── routes.rs # HTTP route handlers (/dns-query, /health) │ ├── dns/ │ │ ├── mod.rs │ │ ├── query.rs # DNS wire-format query parsing │ │ ├── response.rs # DNS response building (SERVFAIL) │ │ └── listener.rs # UDP/TCP DNS listener │ └── upstream/ │ ├── mod.rs │ └── doh_client.rs # DoH forwarding client (retry, strategies) └── tests/ └── integration.rs # HTTP integration tests ``` ## Non-Goals (for now) - DNS-over-TLS (DoT) listener - DNSSEC validation - Persistent cache (Redis / disk) - Web UI / dashboard