5fcbc7be030182286ff37fc73703e76035e9606b
Round-robin now retries with the next resolver when one fails, instead of returning the error immediately. Also reduced connect timeout to 3s and total timeout to 5s for faster failover.
doh-forwarder
A lightweight DNS-over-HTTPS (DoH) forwarder written in Rust.
Accepts DNS queries over HTTPS (RFC 8484) 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
# 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):
[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.
# 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.
# 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.
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
# 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 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