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

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