Mohammadreza Khani 5fcbc7be03 fix: retry with next resolver on failure for both strategies
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.
2026-07-06 18:06:43 +03:30

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 endpointGET /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

Description
A lightweight DNS-over-HTTPS forwarder
Readme MIT 2 MiB
Languages
Rust 55.7%
Shell 37.1%
Perl 7.2%