RouteDNS: a composable DNS pipeline in Go
DNS stub resolver, proxy and router with support for DoT, DoH, DoQ, and DTLS
At a glance
- What is it?
- RouteDNS is a Go DNS stub resolver, proxy and router built as a pipeline of interchangeable components, with listeners for plain DNS, DoT, DoH, DoQ, DoDTLS and Oblivious DoH, and more than thirty processing stages for caching, blocklists, DNSSEC, Lua scripting and load balancing. It suits a network engineer who wants encrypted DNS with policy, and it is a pipeline you assemble rather than a server you switch on.
- Who is it for?
- Adopt RouteDNS if you want encrypted DNS with real policy control, you can express your rules in TOML, and you want a single binary that runs on a router as happily as on a server. Do not adopt it as an authoritative nameserver, a DNS firewall product with a management interface, or a drop-in for a simple stub resolver, because every one of those is a different product.
- Can I use it commercially?
- Yes. BSD-3-Clause 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 4 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 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Every stage implements the same Resolver interface
RouteDNS is a composable DNS stub resolver, proxy and router written in Go, and the whole design comes down to one sentence in the README: every component implements the same Resolver interface, so they can be composed freely.
The architecture diagram splits the system into three kinds of piece. Listeners receive queries over any supported protocol. Routers, groups and modifiers form the processing pipeline, doing routing, filtering, caching and transforming of queries and responses. Resolvers forward queries upstream.
That uniformity is what makes the tool composable. A cache is not a special case, it is a component that takes a query, checks a store, and either answers it or passes it down. A blocklist is a component that inspects and possibly answers. A router is a component that picks a downstream. Because they all present the same face, a pipeline is a chain of them, and the chain is described in configuration rather than in code.
The practical consequence is that the pieces are individually simple and the composition is where the behaviour lives. That is a good trade for the kind of operator who thinks in terms of what happens to a query: you read the TOML file top to bottom and you know the path every query takes.
The scale of the toolbox is large. The README says 30+ other pipeline components, and the feature list names the visible ones: router, cache, blocklist, rate limiter, load balancer, DNSSEC validator and Lua script. Everything is configured via TOML, and the primary language is Go, which is also why the project describes itself as platform independent.
The licence is BSD-3-Clause, the default branch is master, and the project is not archived, with the last push on 2026-10-19 and releases v1.0.1, v1.0.2 and v1.0.3 in September and October 2026.
Protocol coverage including Oblivious DoH and DoH over QUIC
The protocol list is where RouteDNS distinguishes itself from a plain stub resolver, and it is worth reading carefully because the interesting entries are the recent ones.
Plain DNS over UDP and TCP is supported with connection reuse and pipelining, and the topic tags include pipelining, which is a performance property of the client side of a resolver rather than of the server.
DNS-over-TLS follows RFC 7858 and works as both client and server. DNS-over-HTTPS follows RFC 8484, again as client and server, with HTTP/2. DNS-over-QUIC follows RFC 9250, client and server, with 0-RTT support, which lets a client start a query before the handshake completes. DNS-over-DTLS follows RFC 8094, client and server. There is also DNS-over-HTTPS with QUIC transport, and Oblivious DoH follows RFC 9230 with client, proxy and target roles.
Oblivious DoH is the one that changes the threat model rather than just the transport. In a normal DoH setup, the resolver operator can see the client address and the query name together. Oblivious DoH splits that: a target resolver, a client and a proxy in between, so the target does not learn the client's address. The README lists the client, proxy and target roles, which means RouteDNS can play any of the three parts in that arrangement.
Alongside the protocols there is custom CA support and mutual TLS, so you can pin a private certificate authority for an internal resolver, and a SOCKS5 proxy, which matters if the upstream traffic has to go through a tunnel.
For an operator, the practical question is which of these you need and which you must operate. Every protocol you enable is a listening socket and a certificate to manage, and the DoQ and DoDTLS paths in particular are the ones where a bug costs you in ways that are hard to debug from a laptop.
The Go module file shows the dependency weight behind this, with quic-go for QUIC, pion/dtls for DTLS, cloudflare/odoh-go for Oblivious DoH, cisco/go-hpke for HPKE, miekg/dns for the DNS itself, and spf13/cobra for the command line.
Blocklists, response filtering, Lua scripting and DNSSEC
The query processing layer is where the policy work happens, and RouteDNS offers more of it than a typical stub resolver.
Blocklists come in domain, regex, hosts-file and wildcard formats, with auto-refresh from HTTP or file sources. Response blocklists are a separate and more interesting mechanism: they filter by response name, by IP or CIDR, by GeoIP country, or by ASN. That is the difference between blocking a domain you have heard of and blocking a destination range that a domain has just started resolving to, which is the kind of thing that changes when you cannot control the list.
There is also MAC address filtering via EDNS0, so a decision can be made per client on a shared network.
Lua scripting with sandboxed execution for custom query handling logic is the escape hatch, and it is sandboxed, which is the correct way to offer it: a config-driven daemon that runs arbitrary code needs the code to be constrained.
The rest of the processing list covers query and response modification and name translation, static responses using Go templates, EDNS0 Client Subnet manipulation per RFC 7871, and EDNS0 query and response padding per RFC 7830 and RFC 8467. The padding support is a quiet one worth noticing, because response padding is what makes a resolver harder to fingerprint.
DNSSEC validation with IANA trust anchor support is listed under query processing, and DNS64 for NAT64 networks per RFC 6147 synthesises AAAA records from A records, which is what you need on an IPv6-only network whose upstream is IPv4-only.
The last piece of the processing story is that the choice of tool is often not the tool itself but the list you feed it, and the blocklist loaders, including HTTP and local file loaders, are what make that list updatable.
Installing: go install, Linux packages, Homebrew and Docker
There are four installation routes, and the README gives the command for each.
From source with the Go toolchain, which the README says requires Go 1.24 or newer:
go install github.com/folbricht/routedns/cmd/routedns@latestOr a clone and a build from the command directory:
git clone https://github.com/folbricht/routedns.git
cd routedns/cmd/routedns && go installPre-built binaries are available on the GitHub Releases page for Linux on amd64 and arm64, for armv6 and armv7 which covers Raspberry Pi, and for mips and mipsle softfloat which covers OpenWrt routers, plus macOS on amd64 and arm64, FreeBSD and Windows.
The Linux packages are the route that matters if you are running this on a server rather than a workstation, and the README is specific about what they install and how they are started:
sudo dpkg -i routedns_<version>_linux_amd64.deb # Debian/Ubuntu/Raspberry Pi OS
sudo rpm -i routedns_<version>_linux_amd64.rpm # Fedora/RHEL
sudo systemctl enable --now routednsThe packages come as deb, rpm, apk and Arch Linux packages for amd64, arm64 and armv7, and they place the binary at /usr/bin/routedns, a default configuration at /etc/routedns/config.toml, and a systemd service. The naming of the packages follows the usual convention with a version and architecture in the file name, which is what you substitute for the placeholder in the commands above.
On macOS there is a Homebrew formula, brew install folbricht/tap/routedns, and on Docker there are multi-arch images for amd64, arm64 and armv7 on Docker Hub and in the GitHub Container Registry, tagged latest and with the release version:
docker run -d --rm --network host folbricht/routednsThe --network host flag is the important part, because a DNS server bound on loopback inside a bridge network is not reachable by the clients it is meant to serve, and the alternative, publishing port 53, changes the source address your clients appear to come from.
The Dockerfile explains the rest. It cross-compiles on the build host so multi-arch images build without emulation, stamps the version, commit and build date into the binary through linker flags, and the runtime image is a distroless static Debian with no shell and no userland, exposing 53 on both TCP and UDP. The image runs as root by design, because binding the privileged port 53 is what it is for, and it works because the binary is built with CGO_ENABLED=0 and is fully static.
Minimal builds for a router with 16MB of flash
RouteDNS is unusual in offering a documented size-reduction path aimed at a very specific device class, and the numbers in the README are worth reading closely.
Two of the larger dependencies can be left out at build time with Go build tags. The noredis tag leaves out the redis cache backend, and the nolua tag leaves out lua groups. Together they take a linux/arm64 binary from 20.0MB to 14.2MB, and leaving out Redis alone takes a linux/mipsle binary from 21.7MB to 16.7MB, which the README says matters on a router with 16MB of flash.
That last number is the whole point. A full-featured DNS proxy is comfortably larger than the flash on a low-end router, so the size figures are not a nicety, they are the difference between the project being usable on that hardware and not.
The build command is:
go build -tags nolua,noredis -ldflags="-s -w" -o routedns ./cmd/routednsThere is one more constraint, and it is a hard one. Lua needs a 64-bit platform, so 32-bit builds such as arm, mips and mipsle never include it. In other words, on the smallest targets you also lose the scripting escape hatch, and the pipeline you can assemble there is the one without Lua groups.
The releases include routedns-minimal-linux-* binaries built with both tags for arm, arm64, mips and mipsle, so you do not have to reproduce the build yourself.
The behaviour on a reduced binary is clean, and that is the detail I would check first. Configuration is unaffected and all options are still parsed. A config that asks for a feature the binary was built without fails at startup with an error naming it. So the failure mode is a loud, specific startup error rather than a silently ignored section of your configuration, which is the right way to do it, since a silently dropped blocklist or a silently dropped cache would be far worse than a refusal to start.
Where RouteDNS is the wrong tool
The first limitation is what it is not. RouteDNS is a stub resolver, a proxy and a router, not an authoritative nameserver. It does not serve a zone, it does not sign records, and it is not a registrar or a DNS hosting platform. If you need to be the source of truth for a domain, this is the wrong category of software.
The second is that it is a pipeline, and pipelines have a cost. Every stage you add is another place where a query can be answered, dropped, rewritten or delayed, and the operator has to reason about the whole chain. The README's own feature list makes the point without saying it: request deduplication, rate limiting per client subnet, truncate-retry with automatic TCP fallback on truncated UDP responses, TTL min and max clamping, and five different load-balancing algorithms. Each of those is a knob, and knobs multiply.
The third is the operational surface for the less common protocols. DoQ with 0-RTT, DoDTLS and Oblivious DoH are all client and server roles you can turn on, and each carries a certificate, a configuration block and a failure mode. Oblivious DoH in particular needs a target, a client and a proxy, and the README lists all three roles, which means the deployment question is not only how to configure RouteDNS but who plays which part.
The fourth is the platform arithmetic. Lua requires 64-bit, so the smallest supported targets lose it, and a router with 16MB of flash needs the minimal build. The result is that the feature set you can use is a function of the hardware you have, which is a legitimate constraint but one to establish before you design the config.
The fifth is that the tool does not manage anything for you. There is an admin listener with expvar metrics in a Prometheus-compatible form, query and response logging and syslog integration, but there is no dashboard, no policy editor, and no fleet management. If your requirement is to hand a non-specialist a web page where they can block a domain, RouteDNS is a building block for that product rather than the product itself.
Licence, release pace and the dependency surface
The licence is BSD-3-Clause, and this article does not interpret it. For a network component you run on your own infrastructure, a permissive licence with no copyleft obligations is a straightforward choice, and it removes a category of procurement question that affects some other DNS tooling.
The release history is fast. The tags are v1.0.1 on 2026-09-21, v1.0.2 on 2026-10-17 and v1.0.3 on 2026-10-19, with the last push on 2026-10-19 as well. Crossing 1.0 and then shipping two patch releases inside a fortnight suggests a project that has settled into maintenance rather than one still searching for its shape.
One inconsistency is worth knowing about, because it will stop a build. The README says the installation requires Go 1.24 or newer, while the go.mod file in the repository declares go 1.27. The module requirement is the one the toolchain enforces, so install Go 1.27 or newer to be safe.
The dependency list is long and tells you what the feature set costs. Beyond the DNS library and the protocol libraries, there is a Redis client for the shared cache, a Lua interpreter, the HPKE and Oblivious DoH implementations, a GeoIP database reader for the country based response filtering, a SOCKS5 implementation, cobra for the command line, and a DAG library used for the routing graph. There is also an expiration cache library, which is a good sign, since TTL handling in a caching resolver is exactly where subtle bugs live.
The repository layout shows the same shape at the file level. There are dedicated files for each blocklist database variant, including a domain trie implementation and a compact domain format, each with a test file, plus separate cache backends for memory, file, blob and Redis, and a cache-redis-disabled.go for the builds without Redis. There is a dnssec/ directory, an asn-db.go and a cidr-db.go for the GeoIP and ASN filtering, and an adminlistener.go for the metrics endpoint.
The .goreleaser.yaml, the Dockerfile and the release workflow together show that distribution is handled properly, with packages for the major Linux distributions, Homebrew, container images for three architectures, and minimal variants for the small targets. For a project whose whole value is being the one process that every query on your network depends on, that distribution work is not a side concern, and it has been done.
Editorial conclusion
Adopt RouteDNS if you want encrypted DNS with real policy control, you can express your rules in TOML, and you want a single binary that runs on a router as happily as on a server. Do not adopt it as an authoritative nameserver, a DNS firewall product with a management interface, or a drop-in for a simple stub resolver, because every one of those is a different product. Verify first that the Go version you have matches what the module requires, since the README says 1.24 or newer while the go.mod declares go 1.27, that your platform can carry the features you intend to enable, and that the licensing suits you, because the licence is BSD-3-Clause, the newest release is v1.0.3 from 2026-10-19, and the last push was on 2026-10-19.
Frequently asked questions
What is RouteDNS and how is it structured?
RouteDNS is a composable DNS stub resolver, proxy and router written in Go, built from listeners, routers, groups, modifiers and resolvers that all implement the same Resolver interface and are configured via TOML. The README describes more than 30 pipeline component types.
Which DNS protocols does RouteDNS support?
Plain DNS over UDP and TCP with connection reuse and pipelining, DoT per RFC 7858, DoH per RFC 8484 with HTTP/2, DoQ per RFC 9250 with 0-RTT, DoDTLS per RFC 8094, DoH over QUIC, and Oblivious DoH per RFC 9230 in client, proxy and target roles, plus custom CAs, mutual TLS and a SOCKS5 proxy.
How do I install RouteDNS on Linux and start it as a service?
The README gives sudo dpkg -i routedns_<version>_linux_amd64.deb for Debian, Ubuntu and Raspberry Pi OS, sudo rpm -i routedns_<version>_linux_amd64.rpm for Fedora and RHEL, and sudo systemctl enable --now routedns. Packages install the binary to /usr/bin/routedns and a default configuration to /etc/routedns/config.toml, and Docker images are also published for amd64, arm64 and armv7.
What are the minimal builds of RouteDNS for?
The noredis and nolua build tags leave out the Redis cache backend and the Lua groups, taking a linux/arm64 binary from 20.0MB to 14.2MB and a linux/mipsle binary from 21.7MB to 16.7MB, which matters on a router with 16MB of flash. Lua needs a 64-bit platform, so 32-bit builds never include it, and a config asking for a feature the binary lacks fails at startup with an error naming it.
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/folbricht-routedns)Was this useful?