">

How HTTP is Used for DNS: DNS-over-HTTPS in Practice

HTTPS makes it possible to implement secure interaction with the DNS resolver interface, concealing the DNS traffic that would otherwise be transmitted in plain text. This is a fairly specialized technology, but it's already become a standard feature of popular web browsers and is widely used. Let's see how it all works in practice—but not from the browser, from the console—in the process taking apart a basic DNS message.

Neither DNS queries nor DNS responses in classic DNS are protected in any way: DNS transaction data is transmitted in plain text. DNS-over-HTTPS (or DNS Queries over HTTPS, DoH) is currently the most common technology for protecting end-user DNS traffic from eavesdropping and tampering. Even though it is actually TLS that provides protection in DoH, the technology has nothing to do with DNS-over-TLS (DoT). Don't confuse them. For example, a DoH implementation, either on the server or the client, does not require DoT support (and vice versa). Yes, DoH and DoT are similar in their high-level goal: both are technologies for securing DNS transactions, but DoH has a different architecture and therefore a narrower scope of application than DoT.

Unlike DoT, DoH is strictly designed for use on the "last mile," that is, between an application or operating system on the user's computer and the server performing the DNS lookup—a recursive DNS resolver. HTTP in DNS-over-HTTPS is too closely "bound" to DNS for this technology to be applicable, even minimally, between the recursive resolver and authoritative servers. In fact, the original RFC considers only the "last mile" scenario, where DoH proves most useful.

To understand exactly what this "last mile" is, who the authoritative servers and recursive resolvers are, we need to recall the basics of DNS. As a data lookup service, DNS relies on the two logical entities just mentioned: recursive resolvers and authoritative servers. Recursive resolvers collect the information needed to answer a DNS query by obtaining responses from authoritative servers. Classic DNS operates over UDP and, less commonly, over TCP. Authoritative servers (sometimes also called "auth servers") are servers responsible for the addresses in a specific DNS zone, that is, in part of the DNS name hierarchy designated by a DNS name. An example of such a name: example.com. A DNS server becomes authoritative through a process called delegation, when a higher-level authoritative server assigns responsible servers for a given DNS zone. Thus, for example.com, the authoritative servers are b.iana-servers.net. and a.iana-servers.net., which is easy to find out using the dig utility from the BIND package:

$ dig -t NS example.com +short
a.iana-servers.net.
b.iana-servers.net.

The servers labeled a and b are assigned responsibility for example.com. by the authoritative servers of the parent zone—com. (Note that here and later the full form of the domain name is used—FQDN—which must end with a dot on the right; this dot separates the root domain.)

A recursive resolver is a DNS server that performs recursive queries to authoritative DNS servers in order to find the required record. Why the query is called "recursive" will soon become clear. The recursive resolver has its own cache; however, if the answer to the incoming request isn't found in the cache, the resolver starts searching globally in the DNS using a rather complex algorithm. If the system works without errors, the recursive resolver receives from the authoritative server either the required DNS response about the target name, or a so-called "delegation response" with the names of other authoritative servers to which the query should be redirected. This is where the "recursion" in the process name comes from. The recursive resolver usually operates outside the "local machine," as an external service. Often, the recursive resolver service is provided by the internet access provider. But there are also much broader services operating for the Internet as a whole. An example of such a service is Google Public DNS 8.8.8.8, which, among other technologies, supports DoH.

On the local machine, DNS queries are handled by a stub resolver—a much simpler program that only forwards queries to the recursive resolver and accepts responses from it. A stub resolver can also be an element of a larger application, for example, a web browser.

It's exactly between the endpoint stub resolver and the recursive resolver that the so-called "last mile" is located, the protection of which is provided by DoH. That is, over HTTPS, DNS queries are sent to the recursive resolver by the stub resolver (or its logical equivalent). Although a system stub resolver can operate via DoH, the typical example of DoH implementation on the client is still the web browser, which is able to independently form DNS queries and send them directly to a resolver that supports DoH. Any resolver can provide DoH access; it's not necessary to be hosted by Google. So DoH could be deployed even on the resolver of the nearest internet provider, but in practice this is not as common as one might like.

Just like DoT, DoH protects only DNS traffic, not the DNS data itself. That is, DoH provides a secure channel, but doesn't protect the content of DNS queries and responses. If a local web browser uses DoH to make requests to the recursive resolver, the names in the queries and the record values won't be easily accessible to a third party eavesdropping on the traffic. But if DNS transaction data is tampered with directly by the recursive resolver or those data are modified en route from external DNS servers to the resolver, DoH won't help: to protect the actual DNS data, DNSSEC must be used. And DoH only works on the "hop" where it's enabled, meaning on the "last mile"—from the application on the local machine to the resolver. This is a key point: sending a local request to a recursive resolver, if the cache misses, will generate DNS traffic to external authoritative servers, and DoH on the client has no effect on those DNS queries—they will travel through intermediate nodes in plain text.

The basic scheme of working with DoH is simple. A request is sent just like any other HTTP request. The scheme and address for receiving DoH must be configured in the DoH client in some way: you can enter it manually, it can be built into the distribution, as is done in browsers; theoretically, it can even be distributed via DHCP, as is proposed, in theory, in the RFC. Of course, it is also good to know the IP address in advance, since attempting to obtain this address from DNS before launching DoH noticeably reduces the benefit of the latter (see, however, below about address validation). A typical endpoint name is dns-query (but it may differ from service to service). The specification fixes the name of the GET parameter for receiving DoH as dns. So the GET request looks like this:

GET /dns-query?dns=%DNS-message-base64url%

Here %DNS-message-base64url% is the Base64url-encoded DNS message (the actual request).

So, we send a DNS message as a GET parameter, and receive a DNS message with the result as the HTTP response.

It is also possible to use POST, the logic is essentially the same, but as POST prescribes, the useful data is passed not in the URL and encoded as Base64url, but directly in the raw bytes in the body of the request. Everything looks logical here and familiar to anyone who has dealt with HTTP.

A DoH server compliant with the specification must support both GET and POST. Further in this article, only the GET variant is considered, as it is more illustrative; besides, HTTP GET requests are much better suited to caching mechanisms.

Because the interface matches ordinary HTTP use, other REST and similar APIs for interacting with the DNS resolver service are often mistakenly considered implementations of DoH. For example, Google Public DNS provides such an API with JSON at https://dns.google/resolve – you send a GET request with the domain name and other parameters via HTTPS, and receive results from DNS directly in JSON. Convenient. The approach of this API, by itself, seems quite logical. However, this is not modern DoH, despite the name of the description page on developers.google.com. Unfortunately, DoH according to the specification works in a completely different way, and the same Google Public DNS has a separate endpoint for true DoH: https://dns.google/dns-query.

Architecturally, DoH is an intertwining of DNS and HTTP(S). This technology is not a tunnel, like DNS-over-TLS solutions. DoH semantics allow HTTPS parameters to "leak" into DNS, and DNS parameters to "leak" in the opposite direction, into HTTPS. Not the most obvious feature. The transactional scheme of HTTP in DoH directly affects DNS request processing logic. At the same time, DNS requests here are still transmitted in the original, low-level DNS format, but also packed into Base64, and the composition of the request fields is subject to requirements due to HTTP features. For example, HTTP-level caching algorithms are inherited: that is, responses to DoH GET and POST requests come with familiar HTTP statuses, and can be cached like the results of other HTTP transactions, not DNS transactions (it is recommended to cache only the GET variant).

However, this architectural approach also has its advantages. The main one is that DoH traffic becomes indistinguishable from other HTTPS traffic: it uses the same port number and transport protocol (443 and TCP), the same internal markers and queries, differing only in media type (Accept: application/dns-message – see below), which, incidentally, is not visible from the outside. It could even be said that DoH, in this way, implements minimal measures to conceal the fact of its usage – it masquerades as regular website traffic. The main convenience of HTTPS, from this perspective, is that it allows DNS information to better traverse various firewalls put in place at local perimeters and between networks. This method is, as they say, a “double-edged sword,” but effective enough that even certain “malware programs” have begun to use it, since for them both successfully passing traffic through network barriers and hiding the contents of the query are very important in their lifecycle.

The basic steps when using DoH to interact with a recursive resolver are as follows:

  1. The DoH client determines the server access parameters: URI and method (GET or POST);

  2. Since HTTPS requires TLS, the DoH client establishes a TLS connection with the DoH server;

  3. The client forms a DNS message containing the DNS query; in format, this is exactly the same message that a regular DNS client would use over UDP;

  4. The client sends an HTTP request that contains the DNS query as payload through the TLS connection to the server;

  5. The HTTP server extracts the DNS query and passes it to the DNS server, which queries DNS and prepares a DNS response;

  6. The HTTP server places the DNS message inside the HTTP response and sends the response to the client;

  7. The client receives the HTTP response, checks the status and, if possible, extracts the DNS message;

  8. The client closes the HTTPS connection if there are no more requests.

Note that the first, fourth, and following steps directly use HTTP semantics. Even though a DNS message is being transmitted, it always goes over HTTP, as a payload that either constitutes a request (for example, a GET parameter) or is contained in the HTTP response. This is a fundamental feature of DoH.

Practice

In the practical part, we’ll use the Google Public DNS service to determine the IP addresses (A records) corresponding to the name dns.google – that is, not only the addresses, but also the name of this Google service will serve as an example. Requests are sent using curl. We monitor the traffic using tshark. The initial DNS message, since it’s simple, was prepared manually by directly constructing the necessary bytes – this is a hacker approach (in the original sense of the word “hacker”).

Let’s start with the DNS message. Here, only a small part of the possibilities and the simplest format are used. The byte dump (hexadecimal) is given below with brief comments.

1001 // Transaction ID, here set to 0x1001 (see below).
0120 // Flags that set the message parameters (specified: recursion desired (RD), authenticated data support (AD)).
0001 // Number of records in the Question section—one record.
0000 // Number of records in the Answer section—no records, zero.
0000 // Zero records in the Name Servers (NS) section.
0000 // Zero records in the Additional section.
03646e7306676f6f676c6500 // Requested name (see below).
0001 // Query type (so-called QTYPE).
0001 // Query class (so-called QCLASS).

In general, there is plenty of in-depth information to write about DNS message formats and features, but it's not directly related to DoH, so a detailed breakdown will have to wait for other articles. Here, we’ll stick to a brief overview of a few parameters that affect DoT semantics.

The first of these is the two initial bytes of the message: the 16-bit transaction identifier (ID). In classic DNS, this is a simple mechanism that allows matching a query to its response (this is also considered a basic way to protect against “predictive” spoofing, since to send a response with a correct ID you would need to see the request, but its effectiveness is limited because the ID can be guessed). In DoH, HTTP handles the matching of query and response, so it's recommended to set the ID field to zero. But it’s not mandatory. In our example message, it’s 0x1001. The point of setting ID to zero is that HTTP caching does not distinguish a DNS message’s internal semantics—caching is done at the HTTP level. If you use different IDs, you’ll end up with HTTP cache entries that are identical in DNS content but differ due to different ID values. Nevertheless, the DoH server must use the same ID in its response as was present in the DNS query. We’ll check this detail later, when looking at the DoH response.

Next after the ID are the flags, followed by four 16-bit values that indicate the number of records in various sections of the message. A DNS message always contains four sections, top to bottom: “Question”, “Answer”, “Name Servers” (NS/Authority), and “Additional”. In our message, only the Question section is filled. The others are empty. We’ll see the filled Answer section in a response, and the rest aren’t discussed here.

After the section-length list comes the query itself. There’s only one: the name is dns.google., written in DNS format: the individual labels of the name (dns, google) are ASCII strings, each preceded by a byte indicating its length. The end is the root domain, denoted by a single null byte. For example:

0x03 0x64 0x6e 0x73 – three bytes (the length, then the first letter), corresponding to dns.

You’re invited to find the string google (six letters, third is “o”) and the root domain yourself. Here, lowercase letters are used. DNS treats uppercase and lowercase characters, which differ by a single bit in ASCII, as equivalent. That said, you can still use different casing to increase message entropy. This is called case randomization and is also used by Google Public DNS, another reminder of just how complex DNS protocols are—despite the fact that the system looks “very simple” to many.

The two final 16-bit blocks indicate that the record requested for the name is of type A (IPv4), class IN (Internet).

The DoH request is still sent to an IP address. And this IP address, in the context of this experiment, is known in advance: 8.8.8.8. We send the request using curl:

curl --http2 https://8.8.8.8/dns-query?dns=EAEBIAABAAAAAAAAA2RucwZnb29nbGUAAAEAAQ --header "Accept: application/dns-message" --output dns.google.bin

Note that HTTP/2 is used here – it is the minimum recommended HTTP version for DoH. The DNS message dump with the request is encoded into a Base64url string. You can do this encoding, for example, like this:

cat req-1.hex | xxd -r -p | base64 > req-1.b64

In the req-1.hex file there is a dump in hextext (comments, of course, need to be removed); the xxd -r -p utility converts the data to bytes (-r means from text to bytes; -p is the plain hexdump format). Note that Base64url differs from Base64 in details: "+" is replaced with "-", "/" with "_", and there is no padding, so plus signs and slashes do not occur here, but the padding "==", which will be output by the base64 utility at the end of the string, needs to be removed.

As specified, the result of curl is output into the file dns.google.bin, but we will look at the response directly in the traffic (the dump is recorded using tcpdump). You can look into the traffic with tshark, but for tshark to decrypt TLS traffic, session keys are needed. To get the session keys, before running curl you need to set the environment variable SSLKEYLOGFILE=keys.log, where keys.log is the file into which curl (or rather, OpenSSL) will output the session keys.

Calling tshark:

tshark -r doh.pcap -o tls.keylog_file:keys.log -O tls,http2,dns -S "-----PACKET-----" -x

Here doh.pcap is the traffic dump file, -o tls.keylog_file:keys.log is the option showing where to get the keys file, -O is the list of protocols (note that http2 is included); -S and -x are, respectively, the output separator and the format choice with hex dump and ASCII (that is, a matter of taste, and not directly related to parsing DoH traffic).

Scrolling through the tshark output, it’s easy to see the DNS response sent by the 8.8.8.8 service in response to the curl request, inside the HTTP/2 session. Here it is:

 Domain Name System (response)
     Transaction ID: 0x1001
     Flags: 0x81a0 Standard query response, No error
         1... .... .... .... = Response: Message is a response
         .000 0... .... .... = Opcode: Standard query (0)
         .... .0.. .... .... = Authoritative: Server is not an authority for domain
         .... ..0. .... .... = Truncated: Message is not truncated
         .... ...1 .... .... = Recursion desired: Do query recursively
         .... .... 1... .... = Recursion available: Server can do recursive queries
         .... .... .0.. .... = Z: reserved (0)
         .... .... ..1. .... = Answer authenticated: Answer/authority portion was authenticated by the server
         .... .... ...0 .... = Non-authenticated data: Unacceptable
         .... .... .... 0000 = Reply code: No error (0)
     Questions: 1
     Answer RRs: 2
     Authority RRs: 0
     Additional RRs: 0
     Queries
         dns.google: type A, class IN
             Name: dns.google
             [Name Length: 10]
             [Label Count: 2]
             Type: A (Host Address) (1)
             Class: IN (0x0001)
     Answers
         dns.google: type A, class IN, addr 8.8.8.8
             Name: dns.google
             Type: A (Host Address) (1)
             Class: IN (0x0001)
             Time to live: 267 (4 minutes, 27 seconds)
             Data length: 4
             Address: 8.8.8.8
          dns.google: type A, class IN, addr 8.8.4.4
             Name: dns.google
             Type: A (Host Address) (1)
             Class: IN (0x0001)
             Time to live: 267 (4 minutes, 27 seconds)
             Data length: 4
             Address: 8.8.4.4

You can immediately notice the matching ID here: even though the specification recommends writing zero, the request contained 0x1001, and this helped to confirm that the response is genuine, with a matching ID value. The substantive part of the response is two records in the Answer section with the IPv4 addresses of the nodes. Note that using the ID here shows how HTTP and DNS are interconnected in DoH. The ID comes from the DNS request, appears in the DNS response, but in reality, synchronization of the request and response remains up to HTTP; moreover, the ID may interfere with caching, as noted above.

--- Master DoH traffic analysis with ease—unlock powerful insights using [tshark](https://pollinations.ai/redirect/511355) and optimize your DNS-over-HTTPS debugging.

Let's look at some HTTP response headers. First of all, Content-Type contains the value application/dns-message, meaning it is a dedicated type for DoH, so the client can ensure that it has most likely received the correct type of response: don't forget that this is still HTTP, so there can't be any protocol boundaries based on port numbers, for example.

 Header: content-type: application/dns-message
     Name Length: 12
     Name: content-type
     Value Length: 23
     Value: application/dns-message
     content-type: application/dns-message
     [Unescaped: application/dns-message]
     Representation: Literal Header Field with Incremental Indexing - Indexed Name
     Index: 31

Headers related to caching are set according to the parameters of the DNS response. For example, Cache-Control contains a value that matches the TTL from the DNS response. This is another example of protocol intertwining in DoH - the server cannot simply forward the DNS response, as would be the case in a tunnel, but must parse this response in the DNS context (extract the TTL) and appropriately set the HTTP parameters. The header:

 Header: cache-control: private, max-age=267
     Name Length: 13
     Name: cache-control
     Value Length: 20
     Value: private, max-age=267
     cache-control: private, max-age=267
     [Unescaped: private, max-age=267]
     Representation: Literal Header Field with Incremental Indexing - Indexed Name
     Index: 24

In general, a DNS response may contain different TTL values for different records, while in HTTP, the cache for the request is shared. Therefore, the specification prescribes using the minimum TTL value from those sent in the DNS response at the HTTP level.

Since DoH is about DNS access, there is a particular interest in the use of names when working with this protocol. For example, the test request we used above was sent by curl without specifying the server name (SNI). This did not hinder interaction with the service. The DoH resolver 8.8.8.8 responded with a server TLS certificate that contains a rich set of hostnames and IPv4 and IPv6 addresses in the name block:

dNSName: dns.google, dns.google.com, *.dns.google.com, 8888.google, dns64.dns.google
iPAddress: 8.8.8.8, 8.8.4.4, 2001:4860:4860::8888, 2001:4860:4860::8844, 2001:4860:4860::6464, 2001:4860:4860::64

So, if validating the name as prescribed by the HTTPS specification, this certificate is suitable: we accessed the IP address 8.8.8.8, which is listed in the certificate. If access was made by hostname (domain), the name must be validated, regardless of the IP address (this is important), and dns.google is present in the DNS names. But imagine, for a moment, that for initial service discovery, DoH uses "regular" DNS, and later, when sending requests, the connection is established by IP address and the IP address is checked. In this case, a third party could alter the DNS response to specify its own IP address, for which a trusted TLS certificate would be obtained. (That is, the certificate is specifically for the IP address. The certificate can be obtained because this party controls this IP address.) Then, if the DoH client connects without the server name, using only the IP address, and checks for a match in the certificate with the IP address, the connection will be accepted as trusted. Naturally, with a foreign IP or domain name, this trick would not work with proper CA operation. Note that since there are no TLS certificates in DNS, the DoH client must carefully check both the names and addresses in the certificates and when selecting IP addresses.

To sum up. DoH, thanks to the use of TLS, protects DNS traffic from eavesdropping, and server certificate validation, required in HTTPS, allows the connection to be protected against spoofing. However, DoH only works at the hop where it is implemented, and this will almost always be the "last mile", from the local connection to the recursive resolver. Modern recursive resolver packages support access over HTTPS, for example, Unbound. In addition, DoH technology is already widely used on clients—especially when it comes to the web—which makes its deployment easier. DoH does not protect the DNS data itself; to protect the actual DNS content, DNSSEC must be used.

Comments