DNS-over-QUIC in Unbound
By Wouter Wijngaards, with contributions from Yorgos Thessalonikefs
DNS-over-QUIC (DoQ) uses the QUIC transport mechanism to encrypt queries and responses. The DoQ transport for DNS is defined in RFC 9250. With the recent release, Unbound can be configured to support DoQ clients downstream. This feature is not a standard component and must be configured with compilation, with the necessary supporting libraries.
Nevertheless, we are optimistic that our implementation will contribute to maturing the QUIC and DoQ ecosystems. We are dedicated to ensuring that Unbound remains current with the latest advancements in this field.
It would be convenient to be able to use QUIC from OpenSSL, but unfortunately, when the implementation was made, OpenSSL did not have QUIC support. At present, OpenSSL has some QUIC support for client queries, but not server-side. This is a requirement for downstream client support, which Unbound needs. As an alternative, libngtcp2 does provide this functionality, was available at the time of implementation, and is also the library used by libnghttp3. This is an optimal choice given that Unbound already uses libnghttp2 for DNS-over-HTTPS support. The ngtcp2 library gives excellent support for the Unbound state machine set up, in terms of the API and callbacks.
The Unbound implementation of DNS-over-QUIC is designed to enable immediate handling of queries received over the UDP socket where the QUIC protocol is attached, once the DoQ handshakes have completed. If there is a cache response possible, this can be returned in the same event callback from the main Unbound libevent event loop, emitting the outgoing packets at that moment. This should result in a cache response speed that is roughly in line with UDP performance. As anticipated, this will be even more efficient, potentially considerably so, given that the QUIC protocol mandates the preservation of data for potential retransmissions. Unbound allocates this information and keeps it around. With the additional bookkeeping of protocol associations for connections to the clients, it is likely similar to other stream protocols in terms of implementation overhead.
The ngtcp2 library allows for different cryptography support backends. These implement the TLS that is used inside the QUIC connection for encryption. Unbound already uses OpenSSL for crypto, for DNSSEC validation crypto, and also for TLS connections. Today, ngtcp2 uses a modified OpenSSL library for its crypto functions. And it does not use the OpenSSL main codebase, or QUIC routines from there. That means likely in the future, should OpenSSL implement QUIC for server side transactions, we could opt to use that. Or perhaps use a different crypto provider for ngtcp2. The ngtcp2 library and also the modified OpenSSL that it uses are meanwhile not available from package repositories, not as easily as like OpenSSL.
Configuration
The DoQ downstream can be configured, by setting Unbound to listen on the DoQ UDP port for traffic.
With this configuration in unbound.conf
in the server:
section, Unbound will listen on port number 2853 for DoQ traffic. The QUIC port is set using the quic-port
configuration option:
interface: 127.0.0.1@2853
quic-port: 2853
Note that the port number shown here is for test purposes.
It is possible to configure more interfaces with this port number, like::1@2853
, those interfaces are then configured to have DoQ traffic too. If the interface receives also TCP traffic, this can be combined with DNS TCP, or with DNS-over-TLS or with DNS-over-HTTP traffic, by setting the port numbers.
Like for DNS-over-TLS, Unbound needs a TLS certificate for DoQ, and this can be configured with:
tls-service-key: "privatefile.key"
tls-service-pem: "publicfile.pem"
The resource consumption can be configured with quic-size: 8m
. More
queries are turned away.
Libraries
Unbound uses libngtcp2 for DNS-over-QUIC. This in turn requires a modified OpenSSL library for QUIC support in the encryption for the QUIC transport. The modified OpenSSL library is called openssl+quic. It is available for OpenSSL versions 1.1.1 and 3.2.0, and so on.
The modified OpenSSL library is available from the openssl+quic repository, quictls . The libngtcp2 library is available from ngtcp2 .
The online documentation for libngtcp2 is available in their programmers’ guide. The ngtcp2-0.19.1 version tarball can be downloaded from their GitHub releases page, instead of using the git checkout.
For the openssl+quic also tarball downloads are available for releases, like for 3.0.10+quic, openssl-3.0.10-quic1 release.
For example, Unbound can be compiled with version ngtcp2-0.19.1, and with OpenSSL_1_1_1o+quic and openssl-3.0.10-quic1.
This is how to compile openssl+quic:
git clone --depth 1 -b OpenSSL_1_1_1o+quic https://github.com/quictls/openssl openssl+quic
cd openssl+quic
./config enable-tls1_3 no-shared threads --prefix=/path/to/openssl+quic_install
make
make install
Please find a good place for the QUIC installation. The example uses no-shared, so that the shared library search path later does not find the wrong dynamic library, but a shared library works too, of course.
The ngtcp2 library can be compiled like this:
git clone --depth 1 -b v0.19.1 https://github.com/ngtcp2/ngtcp2 ngtcp2
cd ngtcp2
autoreconf -i
./configure PKG_CONFIG_PATH=/path/to/openssl+quic_install/lib/pkgconfig LDFLAGS="-Wl,-rpath,/path/to/openssl+quic_install/lib" --prefix=/path/to/ngtcp2_install
make
make install
Fill in the path to the openssl+quic install and path for where the libngtcp2 install is created. The example sets the rpath to the directory to search for the dynamic library.
The Unbound server can be compiled with DoQ support, with the libngtcp2 library, and the modified OpenSSL library for quic support to libngtcp2, and this OpenSSL library is then also used for TLS and other crypto calls, like for DNSSEC.
Then, compile Unbound like this:
./configure <other flags> --with-ssl=/path/to/openssl+quic_install --with-libngtcp2=/path/to/ngtcp2_install LDFLAGS="-Wl,-rpath -Wl,/path/to/ngtcp2_install/lib" --prefix=/path/to/unbound_install
make
Fill in the path to the openssl+quic install and libngtcp2 install. The rpath is set so that the dynamic libraries can be found in the search path. This then results in an Unbound server that supports DoQ.
Test
Unbound contains a test tool implementation. This can be compiled from the source directory of Unbound, with:
make doqclient
This creates a test tool. You can view the available options by entering:
./doqclient -h
Unbound can be started attached to the console for debug, with:
./unbound -d -c theconfig.conf
With -dd
it prints logs to the terminal as well. Ctrl-C can exit, or send a term signal.
You can send a query with:
./doqclient -s 127.0.0.1 -p 2853 www.example.com A IN
If the server is listening to DoQ queries on port 2853. With -v
the test tool prints more diagnostics.
It is also possible to get more information from the server. This is done by setting configuration for a log file and verbosity 4, or more. It also prints internal information from libngtcp2 for the DoQ transport.
Metrics
The number of QUIC queries is output in num.query.quic
in the statistics. The mem.quic
statistic outputs memory used.
More tests
With our own doqclient
test tool we can verify that Unbound is able to receive and transmit DNS data over QUIC. It can already exercise multiple streams per connection and 0RTT. As with our other similar transfer protocol test tools, it is valuable because we can extend our test suite and handcraft our test cases.
However, since we are both writing the server and client side, it may be the case we are having the same bugs on both sides! 🙀
Going a step further, we wanted to see how Unbound would interoperate with existing DoQ clients. Since DoQ is a cutting edge technology, there are only a couple of clients available at the moment. The results are positive and presented below.
"a modern command-line DNS client (like dig) written in Golang".
It uses a single stream (query) per single DoQ connection. For self-signed server certificates in Unbound, you need to use --skip-hostname-verification
. The tested version was 1.0.5. Example command:
$ doggo <query name> <query type> @quic://<unbound quic IP>:<unbound quic port> --skip-hostname-verification
"Advanced DNS lookup utility" similar in use with dig
; part of the Knot DNS project.
It uses a single stream (query) per single DoQ connection. The tested version was 3.3.9. Example command:
$ kdig +quic -p <unbound quic port> <query name> <query type> @<unbound quic IP>
"Simple DNS proxy with ... DoQ ... support". A simple proxy in the middle software that proxies requests and response between different endpoints.
It uses multiple streams (queries) over a single (or more based on configuration) DoQ connections. It needs a trusted PKI server certificate to establish a DoQ connection to Unbound. The tested version was 0.73.2. Example command:
$ dnsproxy -p <dnsproxy listening port> -u quic://<domain name that resolves to the unbound quic IP and needs to be on the certificate as well>:<unbound quic port>
You can use any tool to start sending queries to dnsproxy that will then forward those queries via DoQ to Unbound.
The future
With Unbound 1.22.0, we are bringing our downstream DoQ design out of the door. We will try to keep the code updated to newer library versions between releases and see if future developments make linking and compiling against DoQ enabled libraries easier and/or part of the operating systems.
We are looking forward for any user feedback that will help us to further test interoperability, stability and performance and shape the client DoQ Unbound experience.