Open-source project
DNSCrypt/doh-server avatar
DNSCrypt/doh-server

doh-proxy: putting DNS-over-HTTPS in front of an existing resolver

Fast, mature, secure DoH and ODoH server proxy written in Rust. Previously known as doh-proxy and rust-doh.

1,076 stars102 forksRustMIT

At a glance

What is it?
doh-proxy is a Rust server that adds DNS-over-HTTPS and Oblivious DoH to a resolver that already exists, with built-in TLS, a Google-compatible JSON API, EDNS client subnet support and configurable caching. It has been in production since February 2018, and it is deliberately not a resolver.
Who is it for?
Adopt doh-proxy if you operate a resolver and want to expose it over DoH or ODoH without changing the resolution path, and put it behind a reverse proxy as the README recommends rather than letting it terminate TLS on its own unless you need the standalone mode. Do not adopt it as a resolver, because it does no DNS resolution of its own, and do not adopt it if you need authoritative DNS, DNSSEC validation or a full recursive resolver, none of which it does.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 12 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

It augments a resolver, it does not replace one

The core sentence in the README is the one that decides whether this tool fits your problem: doh-proxy does not do DNS resolution on its own, but can sit in front of any DNS resolver in order to augment it with DoH support.

That is a narrower job than a resolver and a different one from a forwarder you write yourself. The upstream is a plain DNS address, 9.9.9.9:53 by default, and the proxy's job is to terminate HTTPS, speak the DoH wire format on one side and ordinary DNS on the other, and manage the caching, the timeouts and the connection limits in between. The README says it has been battle-tested in production since February 2018, and the repository has been renamed twice, previously known as doh-proxy and rust-doh, which is a fair sign of a project that has been in service for a long time.

The feature list extends that core job in a few specific directions. There is support for the Google DNS-over-HTTPS JSON API format, so browser-style clients work against it. There is Oblivious DoH, which hides the client IP from the server by adding a relay hop. There is EDNS Client Subnet, which passes a truncated client prefix to the upstream resolver so the answer can be geo-optimised, with an IPv4 prefix length of 24 and an IPv6 prefix length of 56 as defaults. And there is automatic certificate reloading, so updating a TLS certificate does not mean a restart.

Two ways to install it

The README offers precompiled tarballs and Debian packages for Linux and x86_64 from the releases page, which is the route to take on a server. Building from source needs the rust compiler, and there are two variants of the same install command:

sh
cargo install doh-proxy

is the default, with built-in support for HTTPS, while the second disables that support:

sh
cargo install doh-proxy --no-default-features

The second is not a lesser build, it is the one to use when the proxy sits behind a reverse proxy that terminates TLS, and the Cargo.toml shows why the choice is a feature flag. The default feature list is tls, and the tls feature enables the tls feature of the libdoh dependency, which is a local path dependency living in src/libdoh rather than a published crate.

The package is also configured for both rpm and deb packaging, with the binary landing in /usr/bin/doh-proxy and the README in the documentation directory, in the network section with an optional priority. So an organisation that already builds its own packages can pick this up through their normal pipeline rather than installing a binary from the internet.

The flags are the deployment description

The usage reference is a single screen of options, and it reads like a deployment manifest. The most important one is -H, the hostname that DoH clients will connect to, which is a name and not an IP address, so it belongs in a certificate. -u is the upstream resolver, -g sets the external IP address clients connect to and can be given more than once, and -j sets the external port when it is not 443.

The listen address defaults to 127.0.0.1:3000, which is the right default for the reverse proxy deployment because the proxy itself is not meant to be reachable from the internet in that arrangement.

The connection limits are explicit rather than implicit. The maximum number of simultaneous client connections is 512 and the maximum number of concurrent requests per client is 16. The timeout is a time budget of a request in seconds, from 1 to 3600, with a default of 10, and the help text notes that idle connections are closed after that delay and sooner when the server is busy.

The cache bounds are three separate numbers: a minimum TTL of 10 seconds, a maximum TTL of 604800 seconds, and an error TTL of 2 seconds. Those defaults are the difference between a proxy that respects what the upstream said and one that pins short-lived answers for a week.

The documented invocation is short enough to read in one line:

sh
doh-proxy -H 'doh.example.com' -u 127.0.0.1:53

with the built-in TLS variant adding -i for the certificate path and -I for the key path.

Behind a reverse proxy is the recommended arrangement

The README has a deployment section with two options, and it marks the reverse proxy arrangement as recommended. The standalone option uses the built-in TLS support, with the certificate and key paths given on the command line.

The distinction is more than a matter of taste. With built-in TLS, the process owns the certificate lifecycle, and the automatic certificate reloading feature exists so that a new certificate can be picked up without a restart. Behind a reverse proxy, the proxy is terminating plaintext HTTP on its loopback listener, and the certificate handling, the HTTP/2 configuration and the connection limits all belong to the reverse proxy, which is a component more operators already know how to tune and monitor.

The README also has integration sections for sitting in front of an encrypted DNS server and for sitting behind nginx and behind HAProxy, which tells you what the author expects people to combine it with.

Two more operational pieces are documented that are easy to miss. There is a section on timeouts and connection limits, and a section on DNS stamps and certificate hashes, which explains that the certificate hash inside a DNS stamp is what a client pins. The README argues at some length for why those hashes matter, and a stamp published without a hash gives a client nothing to verify against.

JSON API, EDNS client subnet and the privacy trade

Three features deserve separate treatment because each changes what the proxy reveals or how clients must behave.

The JSON API support makes the proxy compatible with the Google DNS-over-HTTPS JSON API format, which is what browsers and several applications use by default. Without it, those clients need to be pointed at a different endpoint or configured for the binary format.

EDNS Client Subnet is enabled with --enable-ecs and lets the proxy forward a truncated client address to the upstream resolver so the answer can be tailored geographically. The prefix lengths are configurable, with 24 bits for IPv4 and 56 bits for IPv6 as the defaults, which are much smaller than a full address but much larger than nothing. The README has its own subsection on privacy considerations, and the honest summary is that ECS trades some client location privacy for answers better tailored to the client's region. It is off unless you ask for it, which is the right default.

Oblivious DoH adds a relay in the path so the server does not learn the client address. The related flag is -O, which allows POST queries over ODoH even when they have been disabled for DoH, and -P disables POST queries altogether.

Where doh-proxy is the wrong tool

The first wrong-tool case is a deployment that needs a resolver rather than a proxy. doh-proxy does not resolve, so if you need recursive resolution, caching of authoritative answers, DNSSEC validation, negative responses handled properly or per-domain blocking, you need a resolver and a proxy in front of it. The README says exactly this, which means the tool is honest about its role rather than pretending to be more.

The second is a network where you cannot run a proxy at all. Some networks intercept DoH and expect clients to talk to a specific resolver, and some client fleets are configured with a fixed provider. In those environments a server-side DoH proxy is not what changes the outcome, since the client decides where the query goes.

The third is a workload that expects more than DNS. The JSON API and the cache controls are about DNS answers, and the project does not describe logging, metrics, rate limiting per identity or any kind of policy engine beyond the connection and concurrency caps. Those are the things an organisation usually wants in front of a public endpoint, and the README points at the operational recommendations and the troubleshooting sections for the rest.

There is also a detail in the packaging worth knowing before you deploy: the repository ships a localhost.pem at the top level, and a public endpoint needs a real certificate for the name your clients connect to.

MIT, version 0.10.0, and a release profile built for speed

The project is MIT licensed, with the LICENSE file at the repository root, and the crate is published as doh-proxy at version 0.10.0 with the author listed as Frank Denis. The release list shows 0.10.0 dated 2026-09-28, 0.9.16 from 2026-05-03 and 0.9.15 from 2025-10-03, and the last push was on 2026-10-11, so work continues after the release.

The release profile in Cargo.toml is worth reading if you build it yourself. It sets codegen-units to 1, disables incremental compilation, turns on fat link-time optimisation, sets opt-level to 3 and switches panic to abort. Every one of those trades build time for runtime speed and binary size, which is the right call for a network-facing daemon and the wrong call if you are rebuilding the crate on every change.

The allocator is also worth noting, since mimalloc is a direct dependency with default features turned off. A proxy handling many small DNS messages is exactly the workload where a general-purpose allocator shows up in profiles.

The repository carries an AI-POLICY.md alongside the code, a contrib/ directory, and the src/ tree with the libdoh sub-crate inside it. The edition is 2018, which is a deliberate choice for a crate that has been compiling for years, and the Rust version is not pinned in the manifest, so a build on a current toolchain is worth a smoke test before you put it in front of traffic.

Editorial conclusion

Adopt doh-proxy if you operate a resolver and want to expose it over DoH or ODoH without changing the resolution path, and put it behind a reverse proxy as the README recommends rather than letting it terminate TLS on its own unless you need the standalone mode. Do not adopt it as a resolver, because it does no DNS resolution of its own, and do not adopt it if you need authoritative DNS, DNSSEC validation or a full recursive resolver, none of which it does. Before you deploy, read the operational recommendations and the timeouts and connection limits sections, decide whether EDNS client subnet is acceptable for your privacy posture, and check the certificate hash section if you publish a DNS stamp, since that hash is what clients pin.

Frequently asked questions

How do I install doh-proxy?

Download precompiled tarballs or Debian packages for Linux and x86_64 from the releases page, or build it with the rust compiler using cargo install doh-proxy. Adding --no-default-features to the cargo install gives a build without the built-in HTTPS support, for use behind a reverse proxy.

Does doh-proxy do its own DNS resolution?

No. The README states that it does not do DNS resolution on its own and instead sits in front of any DNS resolver to augment it with DoH support. The upstream is set with -u, and the default is 9.9.9.9:53.

How do I enable EDNS Client Subnet in doh-proxy?

Pass --enable-ecs, and set the prefix lengths with --ecs-prefix-v4 and --ecs-prefix-v6, which default to 24 and 56. The feature is off unless you ask for it, and the README has a subsection on the privacy considerations.

What are the default cache and connection limits in doh-proxy?

The minimum TTL is 10 seconds, the maximum TTL is 604800 seconds, the error TTL is 2 seconds, the maximum number of simultaneous client connections is 512 and the maximum concurrent requests per client is 16. The request timeout is 10 seconds by default and can be set between 1 and 3600.

Does doh-proxy support the Google DNS-over-HTTPS JSON API?

Yes. The feature list states JSON API support compatible with the Google DNS-over-HTTPS JSON API format, and the README documents a JSON API section with its usage, supported parameters and response format.

Official sources

  1. DNSCrypt/doh-server on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/dnscrypt-doh-server.svg)](https://hysenlabs.com/projects/dnscrypt-doh-server)

Was this useful?