b56bdf628b401782210c7402c65f3754df6e2119
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