RouteDNS acts as a stub resolver and proxy that offers flexible configuration options with a focus on providing privacy as well as resiliency. It supports several DNS protocols such as plain UDP and TCP, DNS-over-TLS and DNS-over-HTTPS as input and output. In addition it's possible to build complex configurations allowing routing of queries based on query name, type or source address as well as blocklists and name translation. Upstream resolvers can be grouped in various ways to provide failover, load-balancing, or performance.
Features:
- Support for DNS-over-TLS (DoT, RFC7858), client and server
- Support for DNS-over-HTTPS (DoH, RFC8484), client and server with HTTP2
- Custom CAs and mutual-TLS
- Support for plain DNS, UDP and TCP for incoming and outgoing requests
- Connection reuse and pipelining queries for efficiency
- Multiple failover and load-balancing algorithms
- Custom blocklists
- In-line query modification and translation
- Routing of queries based on query type, query name, or client IP
- EDNS0 query and response padding (RFC7830, RFC8467)
- Support for bootstrap addresses to avoid the initial service name lookup
- Written in Go - Platform independent
Get the binary, this will put it under $GOPATH/bin, ~/go/bin if $GOPATH is not set:
go get -u github.com/folbricht/routedns/cmd/routedns
Run it:
routedns config.toml
An example systemd service file is provided here
Example configuration files for a number of use-cases can be found here
RouteDNS uses a config file in TOML format which is passed to the tool as argument in the command line. The configuration is broken up into sections, not all of which are necessary for simple uses.
The config file defines listeners which represent open ports and protocols on which incoming DNS queries are received. These queries are then forwarded to routers, groups or resolvers. Routers can redirect queries based on information in the query, while groups can be be used to perform failover between upstream resolvers, or to modify/block queries. A more complex configuration could look like this.
The [resolvers]
-section is used to define and upstream resolvers and the protocol to use when using them. Each of the resolvers requires a unique identifier which may be reference in the following sections. Only defining the resolvers will not actually mean they are used. This section can contain unused upstream resolvers.
The following protocols are supportes:
- udp - Plain (unencrypted) DNS over UDP
- tcp - Plain (unencrypted) DNS over TCP
- dot - DNS-over-TLS
- doh - DNS-over-HTTP
The following example defines several well-known resolvers, one using DNS-over-TLS, one DNS-over-HTTP while the other two use plain DNS. A more extensive list of configurations for public DNS services can be found here.
[resolvers]
[resolvers.cloudflare-dot]
address = "1.1.1.1:853"
protocol = "dot"
[resolvers.cloudflare-doh]
address = "https://1.1.1.1/dns-query{?dns}"
protocol = "doh"
[resolvers.google-udp-8-8-8-8]
address = "8.8.8.8:53"
protocol = "udp"
[resolvers.google-udp-8-8-4-4]
address = "8.8.4.4:53"
protocol = "udp"
Secure resolvers (DoT and DoH) also support additional option to set the trusted CA certificates or even set client key and certificates. Certificate and key files need to be in PEM format. Specify ca
to only trust a specific set of CAs. If not specified, the resolver will use the system trust store.
[resolvers.cloudflare-dot-with-ca]
address = "1.1.1.1:853"
protocol = "dot"
ca = "/path/to/DigiCertECCSecureServerCA.pem"
For full mutual TLS with a private DNS server that expects the client to present a certificate, the client-key
and client-crt
options can be used to specify the key and certificate files.
[resolvers.my-mutual-tls]
address = "myserver:853"
protocol = "dot"
ca = "/path/to/my-ca.pem"
client-key = "/path/to/my-key.pem"
client-crt = "/path/to/my-crt.pem"
When upstream services are configured using their hostnames, routedns will first have to resolve the hostname of the service before establishing a secure connection with it. There are a couple of potenial issues with this:
- The initial lookup is using the OS' resolver which could be using plain unencrypted DNS. This may not be desirable or fail if no other DNS is available.
- The service does not support querying it by IP directly and a hostname is needed. Google for example does not support DoH using
https://8.8.8.8/dns-query
. The endpoint has to be configured ashttps://dns.google/dns-query
.
To solve these issues, it is possible to add a bootstrap IP address to the config. This will use the IP to connect to the service without first having to do a lookup while still preserving the DoH URL or DoT hostname for the TLS handshake. The bootstrap-address
option is available on both, DoT and DoH resolvers.
[resolvers.google-doh-post-bootstrap]
address = "https://dns.google/dns-query"
protocol = "doh"
bootstrap-address = "8.8.8.8"
Multiple resolvers can be combined into a group to implement different failover or loadbalancing algorithms within that group. Some groups are used as query modifiers (e.g. blocklist) and only have one upstream resolver. Again, each group requires a unique identifier.
Each group has resolvers
which is and array of one or more resolver-identifiers. These can either be resolvers defined above, or other groups defined earlier.
The type
determines which algorithm is being used. Available types:
round-robin
- Each resolver in the group receives an equal number of queries. There is no failover.fail-rotate
- One resolver is active. If it fails the next becomes active and the request is retried. If the last one fails the first becomes the active again. There's no time-based automatic fail-back.fail-back
- Similar tofail-rotate
but will attempt to fall back to the original order (prioritizing the first) if there are no failures for a minute.replace
- Applies regular expressions to query strings and replaces them before forwarding the query. Useful to map hostnames to a different domain on-the-fly or append domain names to short hostname queries.blocklist
- A blocklist has just one upstream resolver and forwards anything that does not match its expressions unmodified. If a query matches a block expression, it'll be answered with NXDOMAIN.
In this example, two upstream resolvers are grouped together and will be used alternating:
[groups]
[groups.google-udp]
resolvers = ["google-udp-8-8-8-8", "google-udp-8-8-4-4"]
type = "round-robin"