Self-hosted service
m13253/dns-over-https avatar
m13253/dns-over-https

m13253/dns-over-https: run your own encrypted resolver instead of handing queries to a browser vendor

High performance DNS over HTTPS client & server

2,201 stars245 forksGoMIT

At a glance

What is it?
A Go client and server pair that turns DNS over HTTPS into a local service on port 53, with the configuration surface, the packaging work behind systemd and launchd, and the awkward gap between its last release and its last commit.
Who is it for?
This project is most useful for someone who wants DoH on machines that have no built-in client for it, which in practice means a Linux server, a router, or a container host. It gives you a local resolver that speaks plain UDP on 127.0.0.1 while everything upstream is encrypted, and it lets you choose your upstream instead of accepting whichever provider a browser or operating system decides to hardcode.
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?
Activity is slowing. The repository last received commits 6 months ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

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

Editorial analysis

Two binaries covering both ends of the protocol

The repository description is two words long: high performance DNS over HTTPS client and server. The implementation matches it, and the Go module path carries a major version suffix, github.com/m13253/dns-over-https/v2, which tells you this is the second iteration of a design that has already been revised once.

Protocol coverage is stated up front rather than buried. The README says the software queries DNS over HTTPS using the Google DNS-over-HTTPS protocol and IETF DNS-over-HTTPS, RFC 8484. Those are the two dialects that matter in practice, because the Google wire format was defined by a provider and then standardised, and a server that speaks only one of them is a server that half the ecosystem cannot use. The repository topics name the same set: dns, dns-over-https, doh, google-dns, ietf-doh and rfc-8484.

The layout mirrors the protocol split. doh-client and doh-server are separate top-level directories that each build into their own binary, and json-dns sits alongside them as a third component that handles the JSON representation DNS-over-HTTPS needs in both directions. The Makefile's default target names both outputs explicitly: all depends on doh-client/doh-client and doh-server/doh-server. Depending on both is deliberate, because the client is only half a deployment and running a server on a machine you also resolve from is a reasonable way to test the whole path.

The client installs as a service and takes over local resolution

The client is the component most people actually want. It listens locally on the ordinary DNS port and forwards everything upstream over HTTPS, so applications need no configuration at all. Installing it from source takes a handful of commands, starting with an empty GOPATH:

bash
mkdir ~/gopath
export GOPATH=~/gopath

Then make to build, and sudo make install to lay down the systemd services. The install target in the Makefile does more than copy two binaries into /usr/local/bin. It installs the client and server config files as .conf.example templates, and then copies the real config over the top only if one does not already exist, guarded by a test that the file does not exist. That guard is the difference between upgrading and losing your upstream settings, and it is the kind of detail worth reading in a Makefile before you run a sudo target against a live machine.

The service itself comes up with systemd:

bash
sudo systemctl start doh-client.service
sudo systemctl enable doh-client.service

Then you point the machine at 127.0.0.1 as its resolver, which the README notes is usually done through NetworkManager. The verification step is deliberately mundane:

bash
dig www.google.com
Output:
;; SERVER: 127.0.0.1#53(127.0.0.1)

That SERVER line is the whole acceptance test. If it says 127.0.0.1, queries are going through the local client. Removal is sudo make uninstall, with one wrinkle the README calls out: the configuration files stay at /etc/dns-over-https and have to be removed by hand if you want them gone.

The default upstream is Google, with one named exception

Out of the box the client queries Google DNS over HTTPS at dns.google.com. The README is candid about who that works for and who it does not: it should work for most users, except for People's Republic of China. That sentence is worth reading as a design constraint rather than an aside, because a default upstream that is unreachable from part of the world means every user in that region has to reconfigure before the software does anything for them.

Changing it means editing the config file, which the README opens with sudoedit rather than a plain editor invocation:

bash
sudoedit /etc/dns-over-https/doh-client.conf

The one configuration knob the README documents in detail is upstream selection. If several upstream servers are configured, one is chosen per request according to upstream_selector, and with the default a random server is chosen for each request:

toml
# available selector: random (default) or weighted_round_robin or lvs_weighted_round_robin
upstream_selector = "random"

Three selectors, and they are not interchangeable in meaning. Random spreads load without coordination, which is good for keeping one upstream from learning your query pattern and bad for reproducibility when you are chasing a resolution difference. The two weighted round robin variants imply the upstream set is arranged in tiers, which matches the lvs naming borrowed from layer 4 load balancing. The Docker section adds a related capability at the container level: multiple upstream DNS server support was added to the container image on 2024-12-19, and the run example passes a comma separated upstream list.

Logging is uniform between the two binaries. All log lines, from either doh-client or doh-server, go to stderr, and the README points you at your operating system tool of choice, journalctl when systemd is in charge.

Running the server needs TLS care more than it needs DNS care

The ASCII architecture diagram in the README is the most useful thing in the document. It shows an application talking to a client side cache such as nscd, then to doh-client, then out through an HTTP cache server or content delivery network, to an HTTP service muxer running Apache, Nginx or Caddy, and only then to doh-server sitting in front of a recursive DNS server. That middle layer is the point of the whole design, and the README explains why: although DNS-over-HTTPS can work alone, an HTTP service muxer would be useful since you can host DNS-over-HTTPS along with other HTTPS services on one endpoint.

The requirements that follow are short and strict. HTTP/2 with at least TLS v1.3 is recommended. OCSP stapling must be enabled, otherwise DNS recursion may happen. That second one is the interesting claim, because it is a bootstrap problem stated plainly: if the certificate revocation check needs a fresh DNS lookup and the DNS lookup needs the DoH server, you have a cycle unless the revocation answer is already in the TLS handshake.

The server examples cover three muxer choices and each one is given in full. The Caddy version is three lines because Caddy handles certificates itself:

bash
my.server.name {
	reverse_proxy * localhost:8053
	tls [email protected]
	try_files {path} {path}/index.php /index.php?{query}
}

The Nginx block is the long one, with stapling flags carrying their own minimum version comments, ssl_early_data set to off for 0-RTT, session tickets off, and a resolver line set to 1.1.1.1 with a note to replace it with your local resolver. The Apache block credits Joan Moreau from an issue comment and sets SSLUseStapling on with a shared memory stapling cache. Both long examples are labelled bash by the README's fences even though they are configuration syntax, which is a small reminder that a fence language tag describes intent rather than a parser.

Packaging for four platforms, written as separate sub-Makefiles

Look past the two binaries and the repository is mostly packaging. The tree carries a systemd directory, a NetworkManager directory, a launchd directory, a darwin-wrapper directory and a contrib directory, and the install target in the Makefile fans out to all of them conditionally. On Linux it descends into systemd and NetworkManager, and on Darwin it descends into darwin-wrapper and launchd. Everything else gets only the two binaries and the config files.

The conditionals are ordinary shell inside make recipes, which is worth reading closely if you are packaging the binaries yourself. The configuration directory differs by platform, set to /usr/local/etc/dns-over-https on Darwin and /etc/dns-over-https everywhere else. The Go build command itself is conditional on GOROOT being set, using the Makefile's absolute path to the toolchain when it is and plain go build when it is not, which exists because Debian and Ubuntu users can end up with the Makefile selecting an older Go than the one they just installed. Both binaries are built with -ldflags "-s -w".

The module file is more current than the README's installation note. The README asks for Go at least version 1.20 and says the newer the better, while go.mod declares go 1.24.0. Dependencies are small and tell you the shape of the thing: BurntSushi/toml for the config format, gorilla/handlers for HTTP middleware, miekg/dns for DNS message handling, golang.org/x/net for HTTP/2 and TLS support, and infobloxopen/go-trees for the trie structures behind name lookups and caching. Five direct dependencies is a small surface for infrastructure software, and miekg/dns doing the wire format work means the DNS side is not hand-rolled.

A linter configuration sits in .golangci.yml at the root, which tells you static analysis is part of the workflow rather than an occasional cleanup.

A release history that has stopped and a repository that has not

Here is the part to weigh before you build a dependency on this. The repository publishes tagged releases, and the newest three are v2.3.10 on 2025-03-29, v2.3.9 on 2025-03-28 and v2.3.8 on 2025-01-28. Two patch releases inside roughly a day is a maintenance burst, and then the sequence ends. The most recent push to the repository was on 2026-03-17, close to a year after the last tag.

So the accurate description is that the repository is not archived and still receives commits, while the release sequence stopped in March 2025. Those two facts are compatible and they point in different directions. Anyone installing from a tag gets code that predates a year of commits. Anyone building from master gets code that carries no release notes and no version guarantee, and the v2 in the module path means an untagged breaking change could in principle land in a way that does not affect import paths.

The Changelog.md file at the repository root is where version history actually lives, and the README does not otherwise describe a deprecation or support policy, which is typical for a project of this kind. The maintainer also publishes moving container tags, m13253/dns-over-https-server:latest and m13253/dns-over-https-client:latest, described as builds to try if you feel adventurous. A third-party image, satishweb/doh-server, is what the documented docker run example uses, and that distinction between the official images and the example someone else published is easy to miss and easy to matter.

For a component sitting in front of every name lookup on a machine, an unversioned dependency is a decision you make deliberately rather than one the tooling makes for you.

Editorial conclusion

This project is most useful for someone who wants DoH on machines that have no built-in client for it, which in practice means a Linux server, a router, or a container host. It gives you a local resolver that speaks plain UDP on 127.0.0.1 while everything upstream is encrypted, and it lets you choose your upstream instead of accepting whichever provider a browser or operating system decides to hardcode. If you already have a working DoH setup through an operating system or browser, the honest comparison is against that, not against an alternative project: the argument for running this yourself is control of the upstream and the ability to read the configuration file. The README names two setup guides as starting points, an Apache plus Unbound tutorial and a Docker based one. Weigh the cost before you commit: a local service that every query depends on is a local service that can stop resolving your network, and the DoH literature contains enough disagreement about whether encrypted resolution is a net gain that the decision is worth thinking about rather than defaulting. Check the Changelog for the version history, since the newest tag is v2.3.10 from 2025-03-29 while the most recent push to the repository landed on 2026-03-17, which means commits exist that no release has packaged yet.

Frequently asked questions

Should DNS over HTTPS be turned on?

It depends on what you are protecting against and who controls the resolver. This project exists because DNS-over-HTTPS hides queries inside ordinary HTTPS traffic, which defeats passive network monitoring on the path but also removes the local network's ability to see what names you are looking up. Running a local DoH client on 127.0.0.1 keeps that encrypted hop while leaving ordinary UDP resolution on the loopback interface, so applications need no configuration and the upstream is one you chose.

Why is DNS over HTTPS controversial?

Because encryption changes who can observe and intercept queries. In the default configuration this project's client sends everything to Google DNS over HTTPS at dns.google.com, and the README notes that the default does not work for users in People's Republic of China. Centralisation is the other axis of the argument: a single well known encrypted endpoint is easier to block or observe at scale than many unencrypted resolvers, which is one of the arguments for pointing a self hosted client at a server you control.

What is the default upstream for m13253 dns-over-https?

Google DNS over HTTPS at dns.google.com. The README says it should work for most users except for those in People's Republic of China, and points to /etc/dns-over-https/doh-client.conf for changes. When several upstreams are configured, the upstream_selector setting chooses one per request, with random as the default and weighted_round_robin and lvs_weighted_round_robin as the other options.

How do I verify the DoH client is working?

Point the machine at 127.0.0.1 as its resolver, usually through NetworkManager, then run dig www.google.com and check the SERVER line. The README's expected output shows ;; SERVER: 127.0.0.1#53(127.0.0.1), which confirms queries are going through the local client rather than straight to a configured upstream.

Why should OCSP stapling be enabled on the DoH server?

Because otherwise DNS recursion can happen, as the README puts it. The revocation check for the server certificate needs a name lookup, and if that lookup has to travel to the DoH server you are about to contact, the two depend on each other. Stapling puts the revocation answer inside the TLS handshake, which breaks the cycle. The Nginx and Apache examples in the README both enable it explicitly.

Official sources

  1. License: MIT
  2. m13253/dns-over-https on GitHub
  3. Project website
  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/m13253-dns-over-https.svg)](https://hysenlabs.com/projects/m13253-dns-over-https)