# 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](https://www.rfc-editor.org/rfc/rfc8484)), then forwards them upstream via DoH (HTTPS POST) to configurable resolvers.

## Why?

See [docs/story.md](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

```bash
# 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](config/default.toml):

```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

```bash
# /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:

```bash
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:

```bash
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:

```bash
curl "http://localhost:8800/dns-query?dns=<base64url-encoded-query>"
```

**GET `/health`** — returns `ok`:

```bash
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](DEVELOP.md) for development rules, coding standards, and milestone tracking.

```bash
# 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
