doh-forwarder
A lightweight DNS-over-HTTPS (DoH) forwarder written in Rust.
Accepts DNS queries over traditional DNS (UDP/TCP) and HTTP DoH (RFC 8484), then forwards them upstream via DoH (HTTPS POST) to configurable resolvers.
Why?
See docs/story.md for the full context — why this exists, what already works, and where this project is headed.
Features
- Dual-protocol listener — traditional DNS (UDP/TCP) + HTTP DoH endpoints
- RFC 8484 compliant — GET (
?dns=base64url) and POST (application/dns-message) - Multiple upstreams — configure several DoH resolvers
- Selection strategies — round-robin or failover with automatic retry
- 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 (DoH on :8800, DNS on :53530)
./target/release/doh-forwarder
# Run with a custom config
./target/release/doh-forwarder --config /path/to/config.toml
Configuration
See config/default.toml:
[server]
listen = "0.0.0.0:8800"
[dns]
listen = "0.0.0.0:53530"
[upstream]
resolvers = [
{ name = "cloudflare", url = "https://cloudflare-dns.com/dns-query" },
]
strategy = "round-robin" # or "failover"
Environment Variable Overrides
| Variable | Description | Example |
|---|---|---|
DOH_LISTEN |
Override DoH 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 |
Usage
Point your system DNS at the forwarder
# /etc/resolv.conf
nameserver 127.0.0.1
# Then run the forwarder with [dns] listen = "127.0.0.1:53"
Or use resolvectl with systemd-resolved:
resolvectl dns eth0 127.0.0.1
resolvectl dns-over-tls no # we handle encryption upstream
DoH API
POST /dns-query — raw DNS wire-format in body:
curl -X POST http://localhost:8800/dns-query \
-H "Content-Type: application/dns-message" \
-H "Accept: application/dns-message" \
--data-binary @query.bin
GET /dns-query — base64url-encoded query parameter:
curl "http://localhost:8800/dns-query?dns=<base64url-encoded-query>"
GET /health — returns ok:
curl http://localhost:8800/health
Upstream Strategies
| Strategy | Behavior |
|---|---|
round-robin |
Distributes queries across resolvers in rotation |
failover |
Tries resolvers in order; moves to the next on failure |
Project Structure
doh-forwarder/
├── Cargo.toml
├── config/
│ └── default.toml # Default configuration
├── docs/
│ ├── plan.md # Project plan & milestones
│ └── story.md # Why this project exists
├── src/
│ ├── main.rs # Entrypoint, server setup, graceful shutdown
│ ├── lib.rs # Public API re-exports
│ ├── config.rs # TOML config + env overrides
│ ├── routes.rs # HTTP route handlers (/dns-query, /health)
│ ├── dns/
│ │ ├── mod.rs
│ │ ├── query.rs # DNS 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
└── DEVELOP.md # Development rules & milestone tracking
Development
See DEVELOP.md for development rules, coding standards, and milestone tracking.
# 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
License
MIT
Languages
Rust
55.7%
Shell
37.1%
Perl
7.2%