Dns Over Https

Production engineering guide for dns over https covering patterns, implementation strategies, and operational best practices.

DNS over HTTPS (DoH) is a protocol that encapsulates DNS queries and responses inside HTTP/2 or HTTP/3 requests, enabling encrypted, efficient, and reliable name resolution over the Internet. It matters when network privacy, performance, and resilience are critical—such as in mobile networks, cloud-native applications, and distributed systems where DNS is a frequent, latency-sensitive service. DoH is particularly valuable when clients must resolve names across untrusted networks, such as public Wi-Fi, or when the DNS infrastructure must be flexible, scalable, and interoperable without relying on traditional UDP-based DNS.

Resolving a Domain Name with DoH

To resolve a domain name using DoH, a client sends an HTTP request to a DoH server with a DNS query in the request body. The server responds with a DNS response, also in the HTTP body. The request is typically made over HTTPS using HTTP/2 or HTTP/3, with the Content-Type header set to application/dns-message.

Basic DoH Query Using curl

curl -v \
  -H "Content-Type: application/dns-message" \
  -H "Accept: application/dns-message" \
  --data-binary @query.bin \
  https://dns.example.com/dns-query

The @query.bin file contains a raw DNS message in binary format, generated by a DNS client or library. The --data-binary flag ensures no transformation of the binary payload, which is essential because DNS messages are binary by design.

The server responds with a DNS answer in the HTTP body, and the client parses it to extract the IP address for the requested name.

Constructing a DNS Message

A DNS message consists of a header, question, answer, authority, and additional sections. The header is 12 bytes long and contains fields like id, flags, qdcount, ancount, nscount, and arcount. The question section contains the domain name and query type (e.g., A, AAAA, TXT).

To generate a binary DNS query for example.com of type A, you can use dnsmasq to create a wire-format message:

dnsmasq --dns-rr=example.com,A --dns-port=53 --dns-server=127.0.0.1 \
  --dns-query=example.com,A --dns-protocol=dns --dns-format=bin \
  --dns-output=query.bin

Alternatively, use dig to export a binary query:

dig @1.1.1.1 example.com A +binary > query.bin

The resulting query.bin file is a 28-byte binary blob (header + question), which curl sends to the DoH endpoint.

Server-Side DoH Implementation

A DoH server must handle incoming HTTP requests, parse the DNS message from the body, resolve the query, and return the result in the same format. The server can be built with any HTTP framework. A minimal Go server might look like this:

package main

import (
	"context"
	"encoding/binary"
	"io"
	"log"
	"net/http"
	"syscall"
	"time"
)

func dohHandler(w http.ResponseWriter, r *http.Request) {
	// Read DNS message from request body
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Failed to read request body", http.StatusBadRequest)
		return
	}

	// Parse DNS header
	if len(body) < 12 {
		http.Error(w, "Invalid DNS message: too short", http.StatusBadRequest)
		return
	}

	id := binary.BigEndian.Uint16(body[0:2])
	// Flags, qdcount, ancount, etc. can be extracted from body[2:12]

	// Parse questions
	questionOffset := 12
	questionCount := int(binary.BigEndian.Uint16(body[2:4]))

	var qname string
	for i := 0; i < questionCount; i++ {
		// Read label length
		l := int(body[questionOffset])
		questionOffset++
		// Read label bytes
		label := body[questionOffset : questionOffset+l]
		questionOffset += l
		// Append to qname
		qname += string(label) + "."
		// Skip type and class
		questionOffset += 2 + 2
	}

	// Simulate DNS resolution
	ip := resolve(qname)

	// Build response
	response := make([]byte, 12 + 12 + 4) // Header + question + answer
	copy(response, body[:12]) // Copy original header
	response[2] = 0x80 // Set QR flag (response)
	response[4] = 0x01 // Set qdcount = 1
	response[5] = 0x01 // Set ancount = 1

	// Add answer RR
	offset := 12 + 12 // after header and original question
	response[offset] = 0xC0 // compressed name pointer
	response[offset+1] = 0x0C // to start of question
	offset += 2
	response[offset] = 0x00 // type: A
	response[offset+1] = 0x01
	response[offset+2] = 0x00 // class: IN
	response[offset+3] = 0x01
	response[offset+4] = 0x00 // TTL: 1 hour
	response[offset+5] = 0x01
	response[offset+6] = 0x00
	response[offset+7] = 0x00
	response[offset+8] = 0x00 // RDLENGTH: 4
	response[offset+9] = 0x04
	// IP address (e.g., 93.184.216.34)
	copy(response[offset+10:], ip)

	// Write response
	w.Header().Set("Content-Type", "application/dns-message")
	w.Header().Set("Content-Length", string(len(response)))
	w.WriteHeader(http.StatusOK)
	w.Write(response)
}

func resolve(name string) []byte {
	// Simulate lookup
	switch name {
	case "example.com":
		return []byte{93, 184, 216, 34} // 93.184.216.34
	case "cloudflare.com":
		return []byte{104, 154, 16, 15}
	default:
		return []byte{127, 0, 0, 1}
	}
}

func main() {
	http.HandleFunc("/dns-query", dohHandler)
	log.Fatal(http.ListenAndServe(":443", nil))
}

This handler reads the DNS message from the HTTP body, parses the question, resolves the domain name, constructs a response with an A record, and sends it back in binary DNS format.

Configuring a DoH Client

A DoH client must be able to:

The most common configuration is a list of DoH endpoints, often provided via a doh.json file:

{
  "servers": [
    {
      "name": "Cloudflare",
      "url": "https://cloudflare-dns.com/dns-query",
      "ca": "/etc/ssl/certs/cloudflare-ca.pem",
      "timeout": 3000,
      "retry": 2
    },
    {
      "name": "Google",
      "url": "https://dns.google/dns-query",
      "ca": "/etc/ssl/certs/google-ca.pem",
      "timeout": 2000,
      "retry": 3
    }
  ]
}

The client uses the Content-Type: application/dns-message and Accept: application/dns-message headers to signal the format. It can also set User-Agent to identify the client software.

Discovering DoH Servers via DNS

A DoH client can discover the DoH server for a domain using DNS TXT records. The standard is defined in RFC 8484 and specifies that a client queries for a TXT record at _doh._tcp.<domain>.

For example, to discover DoH servers for example.com, the client queries:

dig TXT _doh._tcp.example.com

The response might be:

_doh._tcp.example.com. 300 IN TXT "priority=10, weight=5, target=do.example.com., path=/dns-query"

The client parses the TXT record to extract target, path, and priority. It then constructs the DoH URL as https://target/path, using the path and target fields.

If multiple records exist, the client selects the one with the highest priority and, in case of ties, the highest weight.

Error Handling and Failure Modes

DoH introduces several failure points not present in traditional DNS:

HTTP-Level Failures

DNS-Specific Failures

Silent Failures

HTTP/2 and HTTP/3 Considerations

Performance and Interoperability

DoH reduces DNS latency by eliminating the need for separate UDP connections and enabling persistent HTTP sessions. It also enables seamless integration with existing web infrastructure—CDNs, load balancers, and TLS termination.

Clients can cache DoH responses using HTTP caching headers (Cache-Control, ETag, Expires). Servers can use ETag headers to provide efficient incremental updates.

A DoH client can use Cache-Control: max-age=3600 to cache answers for one hour. If the server’s response includes an ETag, the client can send a If-None-Match header on subsequent queries:

GET /dns-query HTTP/2
Host: dns.example.com
Content-Type: application/dns-message
Accept: application/dns-message
If-None-Match: "abc123"

If the ETag matches, the server responds with 304 Not Modified, reducing bandwidth.

Advanced Use Cases

EDNS0 Support

Extended DNS (EDNS0) allows larger messages, additional options, and improved error handling. A DoH client can include EDNS0 in the query by adding an OPT record in the additional section.

For example, to request a 4096-byte buffer size:

; OPT record
OPT 0x0000 0x0001 0x0000 0x0000 0x0000 0x0000 0x0000
  - UDP size: 4096
  - Extended RCODE: 0
  - Version: 0
  - Flags: 0

The server uses this information to send larger responses and to include additional EDNS options like DNSSEC, DNSCOOKIE, and PADDING.

Batched Queries

A client can send multiple DNS queries in a single HTTP request. The server returns a single response with all answers. This is useful for resolving multiple domains in parallel.

The client builds a DNS message with multiple questions in the question section. The server processes all questions and returns a single response with all answers, authority, and additional records.

Dynamic Configuration via HTTP

A DoH server can expose a /config endpoint that returns a JSON document with current server configuration, including:

Clients can periodically poll this endpoint to adjust their behavior dynamically.

Summary

DNS over HTTPS is a robust, standardized way to deliver encrypted, reliable, and high-performance DNS resolution. It works across any HTTP/2 or HTTP/3 capable infrastructure, and its success hinges on precise handling of binary DNS messages, correct HTTP headers, and proper error reporting. The protocol excels in environments where DNS is a critical path service, and its integration with existing web infrastructure makes it a natural choice for modern, distributed systems.

This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.