dnsproxy: a Go DNS forwarder that speaks DoH, DoT, DoQ and DNSCrypt
Simple DNS proxy with DoH, DoT, DoQ and DNSCrypt support
At a glance
- What is it?
- AdguardTeam's dnsproxy is a single Go binary that sits between a client and an upstream resolver, adding encrypted transports, caching and upstream control. It is the transport layer, not the resolver.
- Who is it for?
- dnsproxy is the right tool when the problem is transport and routing: an encrypted server for a router or a phone, a way to move clients off plaintext port 53, or a way to pin specific domains to specific resolvers. It is the wrong tool when you need filtering, caching policy you cannot tune by hand, or recursive resolution, because the README describes forwarding to upstreams and says nothing about recursive lookup.
- Can I use it commercially?
- Yes. Apache-2.0 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 3 days 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 31, 2026, and from our analysis. They are not legal advice.
Star history
- AdguardTeam/dnsproxy3,386 ★ · +30
| Repository | 2026-10-07 | 2026-10-31 |
|---|---|---|
| AdguardTeam/dnsproxy | 3.4k | 3.4k |
30 Stars in the last 24 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
A forwarder between a client and an upstream resolver
The README's one-line description is precise about the shape of the thing: a DNS proxy server that supports all existing DNS protocols, including `DNS-over-TLS`, `DNS-over-HTTPS`, `DNSCrypt` and `DNS-over-QUIC`, and which can also work as a server for any of the encrypted three. That is a two-sided design. The same process can accept plaintext or encrypted queries from clients, and reach its upstreams over plaintext or encrypted links, which is what makes it useful as a migration tool rather than just a server.
What the README does not describe is recursion. There is no mention of root hints, zone transfers, or an authoritative server mode, and the flag list contains no option for either. Upstreams are named with `--upstream` and, when they are unavailable, `--fallback` takes over. So the honest summary is that dnsproxy forwards. It is the pipe between a client and a resolver that already exists, which is a narrower job than running a resolver and a different job from filtering.
The repository layout matches that reading. `upstream/` is the forwarding and mode logic, `proxy/` holds the protocol listeners, `fastip/` implements the fastest-address strategy, `ratelimit/` the request throttling, `internal/` the wiring, and `main.go` the entry point. There is no filtering or ruleset package in the tree, which is consistent with a project whose job is transport.
Building from source needs Go 1.26, not a package manager
The install section lists three routes: a prebuilt binary from the releases page, the official Docker image at `adguard/dnsproxy`, or building it yourself. The build route is short because the Makefile does the work:
make buildThe stated requirement is Go 1.26 or later, and the Makefile is stricter than that, pinning `GOTOOLCHAIN = go1.26.8` so a build on an older local toolchain fetches the right one rather than failing. It also fixes `OUT = dnsproxy` and `DIST_DIR = build`, keeps a `.POSIX` header so the file stays portable across make implementations, and exposes `RACE` and `VERBOSE` macros. There is a `staticcheck.conf` in the tree, so lint configuration is part of the repo rather than something the maintainer keeps private.
A plain build from a fresh clone is the other route:
git clone https://github.com/AdguardTeam/dnsproxy
cd dnsproxy
make buildThe Go module is `github.com/AdguardTeam/dnsproxy` and `go.mod` asks for Go 1.26.8 directly. The dependency list says a lot about the transport claims: `github.com/miekg/dns` v1.1.72 for the DNS wire format, `github.com/quic-go/quic-go` v0.60.0 for DoQ, `github.com/AdguardTeam/dnscrypt` v0.0.2 for DNSCrypt, and `github.com/ameshkov/dnsstamps` v1.0.3 for server stamps. TLS and HTTP work comes through `golang.org/x/net` v0.57.0. None of these are optional extras; they are the four encrypted transports in the README's first paragraph.
Upstream modes decide who answers a query
The most consequential flag after the listeners is `--upstream-mode`. Three values are offered and the default is `load_balance`. In practice that means queries are dealt out across the configured upstreams rather than all going to the first one, which spreads load and avoids sending every request to a single resolver.
`parallel` changes the timing, sending to upstreams together and using the fastest answer. `fastest_addr` is the more interesting one because it answers on addresses rather than on whole responses, and it has its own package in the tree, `fastip/`, to implement it. On a dual-stack or slow-link network this is often the difference between a page that paints quickly and one that waits for a resolver that returned an unreachable address first. Which of these you want depends on whether your bottleneck is resolver latency or address choice, and the README does not pretend to have a single right answer.
The rest of the routing flags build the same picture. `--bootstrap` supplies bootstrap DNS for DoH and DoT, needed to resolve the server name before the encrypted channel exists. `--fallback` takes over when the regular upstreams are unavailable and accepts either repeated flags or a file listing servers. `--hosts-file-enabled` with `--hosts-files` adds hosts files to resolution. `--use-private-rdns` with `--private-subnets` and `--private-rdns-upstream` sends reverse lookups of private addresses somewhere private rather than leaking them. `--edns` with `--edns-addr` passes a client subnet upstream, which matters for CDNs but is also a privacy decision.
Cache flags that arrive with their own warnings attached
Caching is off unless `--cache` is passed. When it is on, the size defaults to 64k bytes and entries are bounded by `--cache-max-ttl` and `--cache-min-ttl`. The description of the minimum is worth quoting rather than paraphrasing: it is capped at 3600, and the help text says that artificially extending TTLs should only be done with careful consideration. That is a caveat about stale answers and about resolvers upstream you do not control.
Optimistic caching is a separate switch, `--cache-optimistic`, and it is the more interesting one. An expired entry is served immediately rather than waiting for a refresh, with `--optimistic-answer-ttl` giving the default TTL for those expired entries at 30 seconds and `--optimistic-max-age` dropping entries after 12 hours. For a router in front of a residential broadband line, that trades a small amount of correctness for a large drop in visible latency.
Two security-related flags belong in the same breath. `--pending-requests-enabled` makes the server collapse duplicate queries, sending only the first upstream and passing the answer to the rest, and the help text states plainly that disabling it introduces a vulnerability to cache poisoning attacks. That is an unusually blunt warning for a flag help string, and it tells you the default posture. `--dnssec` sets the DO bit on upstream requests and defaults to true, while `--bogus-nxdomain` converts responses containing addresses in given CIDRs into NXDOMAIN, the standard trick for short-circuiting NXDOMAIN blocking. `--refuse-any` refuses ANY queries. `--ratelimit`, `--ratelimit-subnet-len-ipv4` and `--ratelimit-subnet-len-ipv6` cap requests per second, and the per-subnet lengths matter because a global cap punishes every client for one noisy client.
Serving encrypted DNS from the same process
The server side is where the flag list gets long, and the pattern is consistent. Each transport has a port: `--https-port` or `-s` for DoH, `--tls-port` or `-t` for DoT, `--quic-port` or `-q` for DoQ, `--dnscrypt-port` or `-y` for DNSCrypt. Certificates come from `--tls-crt` or `-c` and `--tls-key` or `-k`, with `--tls-min-version` and `--tls-max-version` to bound the negotiation. `--http3` adds HTTP/3 support on top of DoH, which is the modern way to avoid head-of-line blocking on the HTTPS connection itself.
DoH routes are explicit. `--doh-routes` registers `GET /dns-query` and `POST /dns-query` by default, and the help text also lists `GET /` and `POST /` as deprecated and soon to be removed. `--https-userinfo` requires basic authentication on every DoH query, which turns the endpoint into something you can put behind a credential rather than an open resolver. `--https-server-name` sets the `Server` header, and `--doh-insecure-enabled` skips certificate verification on the DoH server side, which is for lab use and should stay off.
One mode does not fit the pattern. `--dns64` makes dnsproxy act as a DNS64 server, translating AAAA lookups into A lookups under a prefix that defaults to the well-known `64:ff9b::` and can be specified more than once. This is the piece that makes an IPv6-only client work on a network with no native IPv6, and it is the only feature here that changes query semantics rather than transport.
The operational flags are small and worth knowing: `--timeout` for outbound queries, `--max-go-routines` to cap concurrency, `--udp-buf-size` when the system default is too small, `--pprof` which exposes profiling on localhost:6060, and `--output` or `-o` for the log file. Configuration can also live in a YAML file via `--config-path`, with `config.yaml.dist` in the repository as the minimal working example, and command line options override whatever the file says. For a router, that file is the realistic way to run this.
v0.84.0 unexported the proxy fields, which narrows library use
dnsproxy is usable as a Go package, and the releases tell you how much room that leaves. v0.84.0, published 2026-08-14, changed all `proxy.Proxy` fields to unexported. For anyone embedding the proxy in their own binary, that is a breaking change to a struct you can no longer populate field by field. v0.84.1 on 2026-08-19 added the `dnsproxytest` package, a testing helper, and v0.84.2 on 2026-09-04 updated Go to 1.26.8 and fixed DoQ 0-RTT opcode handling. A patch release fixing opcode handling in zero-round-trip mode tells you DoQ is exercised, and that 0-RTT has the sharp edges you would expect from a path that skips the handshake.
The consequence for adopters is straightforward. If you want the binary, nothing changes: it is Apache-2.0 licensed, the flags are stable enough that a minor release fixed a protocol edge rather than renaming options. If you want the library, read the current exported surface before designing around it, because a release that unexports fields can reach into your code without a major version bump.
The honest comparison is with the category rather than a single product. A recursive resolver such as Unbound owns the whole path from root hint to answer and brings its own cache and DNSSEC validation. A filtering product brings rulesets and a dashboard. dnsproxy deliberately sits beside both, doing encrypted transport, upstream selection and optional caching, which is exactly what you want on a small network where an existing resolver already does the hard part. Given how much configuration surface `--upstream-mode`, `--cache-optimistic`, `--dns64` and the per-transport ports expose, the routing flags are where the real design work happens, and the README documents them clearly enough to plan against.
Editorial conclusion
dnsproxy is the right tool when the problem is transport and routing: an encrypted server for a router or a phone, a way to move clients off plaintext port 53, or a way to pin specific domains to specific resolvers. It is the wrong tool when you need filtering, caching policy you cannot tune by hand, or recursive resolution, because the README describes forwarding to upstreams and says nothing about recursive lookup. The repository is not archived and the last push was on 2026-09-22, with v0.84.2 released on 2026-09-04. Start with the flag list above, then read config.yaml.dist, since the file and the command line can express the same settings and the file is the tidier long-term home.
Frequently asked questions
What is a DNS proxy?
A DNS proxy sits between clients and upstream resolvers, forwarding queries and answers so the two sides can use different transports. AdguardTeam's dnsproxy is one such tool: the README describes it as a simple DNS proxy server supporting DNS-over-TLS, DNS-over-HTTPS, DNSCrypt and DNS-over-QUIC, and able to act as a server for any of the encrypted ones. It forwards rather than resolving, since it has no recursive mode.
How do I configure a DNS proxy?
dnsproxy is configured with command line flags or with a YAML file passed to `--config-path`, where `config.yaml.dist` in the repository is the minimal working example, and command line options override the file. Upstreams are set with `--upstream`, `--upstream-mode` chooses `load_balance`, `parallel` or `fastest_addr`, and each transport has its own port flag such as `--https-port`, `--tls-port` and `--quic-port`.
Can dnsproxy be run as a Go library inside another service?
Yes, the repository ships importable packages including `proxy/`, `proxyutil/`, `upstream/` and `fastip/`, and release v0.84.1 added a `dnsproxytest` package. Note that v0.84.0 made all `proxy.Proxy` fields unexported, so code that constructed the struct directly needs to use the current exported surface instead.
Official sources
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.
[](https://hysenlabs.com/projects/adguardteam-dnsproxy)Star history chart for your README
[](https://hysenlabs.com/projects/adguardteam-dnsproxy)Was this useful?