# doh-forwarder A lightweight DNS-over-HTTPS (DoH) forwarder written in Rust. Accepts DNS queries over HTTPS ([RFC 8484](https://www.rfc-editor.org/rfc/rfc8484)) and forwards them to upstream DoH resolvers (Cloudflare, Google, Quad9, or custom). ## Features - **RFC 8484 compliant** — GET (`?dns=base64url`) and POST (`application/dns-message`) - **Multiple upstreams** — configure several DoH resolvers - **Selection strategies** — round-robin or failover - **In-memory cache** — TTL-aware with LRU eviction (configurable) - **Environment overrides** — override config via env vars - **Graceful shutdown** — handles SIGINT and SIGTERM - **Health endpoint** — `GET /health` ## Quick Start ```bash # Build cargo build --release # Run with default config ./target/release/doh-forwarder # Run with a custom config ./target/release/doh-forwarder --config /path/to/config.toml ``` ## Configuration Create a TOML config file (see [config/default.toml](config/default.toml)): ```toml [server] listen = "0.0.0.0:3000" [upstream] resolvers = [ { name = "cloudflare", url = "https://cloudflare-dns.com/dns-query" }, { name = "google", url = "https://dns.google/dns-query" }, { name = "quad9", url = "https://dns.quad9.net/dns-query" }, ] strategy = "round-robin" # or "failover" [cache] enabled = true max_entries = 10000 max_ttl_secs = 3600 ``` ### Environment Variable Overrides | Variable | Description | Example | |---|---|---| | `DOH_LISTEN` | Override server listen address | `0.0.0.0:8080` | | `DOH_UPSTREAM_STRATEGY` | Override upstream strategy | `failover` | | `DOH_UPSTREAM_RESOLVERS` | Comma-separated `name=url` pairs | `google=https://dns.google/dns-query,cf=https://cloudflare-dns.com/dns-query` | | `RUST_LOG` | Log level filter | `debug`, `doh_forwarder=trace` | ## API ### POST `/dns-query` Send a raw DNS wire-format query in the request body. ```bash # Using a pre-built DNS query (hex-encoded) curl -X POST http://localhost:3000/dns-query \ -H "Content-Type: application/dns-message" \ -H "Accept: application/dns-message" \ --data-binary @query.bin ``` ### GET `/dns-query` Send a base64url-encoded DNS query as a query parameter. ```bash # Encode a DNS query and send via GET QUERY=$(python3 -c " import base64, struct # Minimal A record query for example.com q = b'\\x12\\x34' # ID q += b'\\x01\\x00' # flags q += b'\\x00\\x01' # 1 question q += b'\\x00\\x00' # 0 answers q += b'\\x00\\x00' # 0 authority q += b'\\x00\\x00' # 0 additional q += b'\\x07example\\x03com\\x00' # name q += b'\\x00\\x01' # type A q += b'\\x00\\x01' # class IN print(base64.urlsafe_b64encode(q).rstrip(b'=').decode()) ") curl "http://localhost:3000/dns-query?dns=$QUERY" ``` ### GET `/health` Returns `ok` if the server is running. ```bash curl http://localhost:3000/health # ok ``` ## Upstream Strategies | Strategy | Behavior | |---|---| | `round-robin` | Distributes queries across resolvers in order | | `failover` | Tries resolvers in order; moves to the next on failure | ## Development ```bash # Prerequisites rustup show # Rust 1.80+ # Run with auto-reload cargo install cargo-watch cargo watch -x run # Run tests cargo test # Lint & format cargo clippy -- -D warnings cargo fmt --check ``` See [DEVELOP.md](DEVELOP.md) for full development rules and milestone tracking. ## Project Structure ``` doh-forwarder/ ├── Cargo.toml ├── config/ │ └── default.toml # Default configuration ├── src/ │ ├── main.rs # Entrypoint, server setup │ ├── lib.rs # Public API re-exports │ ├── config.rs # TOML config + env overrides │ ├── cache.rs # DNS response cache (TTL + LRU) │ ├── dns/ │ │ ├── mod.rs │ │ ├── query.rs # DNS query parsing │ │ └── response.rs # DNS response building │ ├── upstream/ │ │ ├── mod.rs │ │ └── doh_client.rs # DoH forwarding client │ └── routes.rs # HTTP route handlers └── tests/ └── integration.rs # Integration tests ``` ## License MIT