DoH Wire Format (RFC 8484)

Standard DNS-over-HTTPS using wire format. Compatible with DNS libraries and system resolvers.

TL;DR

Send raw DNS wire format queries over HTTPS. Base64url-encode for GET, binary for POST.

# GET with base64url-encoded query
curl "https://api.resolvedb.io/dns-query?dns=AAABAAABAAAAAAAAA2dldAZ3ZWF0aGVyBnB1YmxpYwJ2MQlyZXNvbHZlZGIDbmV0AAAQAAEAADApAACOAAABAAEAACkQ..." \
  -H "Accept: application/dns-message"

# POST with binary body
curl -X POST "https://api.resolvedb.io/dns-query" \
  -H "Content-Type: application/dns-message" \
  -H "Accept: application/dns-message" \
  --data-binary @query.bin

When to Use Wire Format

  • System resolver configuration - Configure OS/browser to use ResolveDB as DoH resolver
  • DNS libraries - Libraries like dnspython, trust-dns, miekg/dns support DoH
  • Compact responses - Binary format is smaller than JSON
  • RFC compliance - Standard RFC 8484 implementation

For simple queries, the JSON API is easier.

Endpoint

https://api.resolvedb.io/dns-query

GET Request

Encode the DNS query in Base64url (no padding) and pass as dns parameter.

Format

GET /dns-query?dns={base64url-encoded-query}
Accept: application/dns-message

Example

# 1. Create DNS query (or use a library)
# This queries: get.newyork.weather.public.v1.resolvedb.net TXT

# 2. Base64url encode (no padding)
DNS_QUERY="AAABAAABAAAAAAAAA2dldAZ3ZWF0aGVyBnB1YmxpYwJ2MQlyZXNvbHZlZGIDbmV0AAAQAAEAADApAACOAAABAAEAACkQ"

# 3. Send request
curl "https://api.resolvedb.io/dns-query?dns=${DNS_QUERY}" \
  -H "Accept: application/dns-message" \
  --output response.bin

Query Size Limit

Maximum 4KB decoded query size. The base64url-encoded parameter can be up to 8KB.

POST Request

Send raw DNS wire format as request body.

Format

POST /dns-query
Content-Type: application/dns-message
Accept: application/dns-message

{binary DNS message}

Example

# Create query binary file using Python
import dns.message
import dns.rdatatype

query = dns.message.make_query('get.newyork.weather.public.v1.resolvedb.net', dns.rdatatype.TXT)
with open('query.bin', 'wb') as f:
    f.write(query.to_wire())
# Send request
curl -X POST "https://api.resolvedb.io/dns-query" \
  -H "Content-Type: application/dns-message" \
  -H "Accept: application/dns-message" \
  --data-binary @query.bin \
  --output response.bin

Request Size Limit

Maximum 4KB request body.

Response Format

Both GET and POST return:

Content-Type: application/dns-message
Cache-Control: max-age={min-ttl}

{binary DNS message}

For errors:

Cache-Control: no-store

Building DNS Queries

Python (dnspython)

import dns.message
import dns.query
import dns.rdatatype

# Build query
query = dns.message.make_query(
    'get.newyork.weather.public.v1.resolvedb.net',
    dns.rdatatype.TXT
)

# Send via DoH
response = dns.query.https(
    query,
    'https://api.resolvedb.io/dns-query'
)

# Parse response
for rrset in response.answer:
    for rdata in rrset:
        print(rdata.to_text())

Go (miekg/dns)

package main

import (
	"encoding/base64"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"

	"github.com/miekg/dns"
)

func main() {
	// Build query
	msg := new(dns.Msg)
	msg.SetQuestion("get.newyork.weather.public.v1.resolvedb.net.", dns.TypeTXT)
	msg.RecursionDesired = true

	// Pack to wire format
	wire, err := msg.Pack()
	if err != nil {
		panic(err)
	}

	// GET request with base64url
	encoded := base64.RawURLEncoding.EncodeToString(wire)
	reqURL := fmt.Sprintf("https://api.resolvedb.io/dns-query?dns=%s", url.QueryEscape(encoded))

	req, _ := http.NewRequest("GET", reqURL, nil)
	req.Header.Set("Accept", "application/dns-message")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	// Read and parse response
	body, _ := io.ReadAll(resp.Body)
	response := new(dns.Msg)
	if err := response.Unpack(body); err != nil {
		panic(err)
	}

	// Print TXT records
	for _, rr := range response.Answer {
		if txt, ok := rr.(*dns.TXT); ok {
			fmt.Println(strings.Join(txt.Txt, ""))
		}
	}
}

Rust (trust-dns)

use trust_dns_client::client::{Client, SyncClient};
use trust_dns_client::op::DnsResponse;
use trust_dns_client::rr::{DNSClass, Name, RecordType};
use trust_dns_client::h2::HttpsClientConnection;
use std::str::FromStr;

fn main() {
    // Configure DoH client
    let conn = HttpsClientConnection::new(
        "https://api.resolvedb.io/dns-query".parse().unwrap(),
    );
    let client = SyncClient::new(conn);

    // Build and send query
    let name = Name::from_str("get.newyork.weather.public.v1.resolvedb.net.").unwrap();
    let response: DnsResponse = client
        .query(&name, DNSClass::IN, RecordType::TXT)
        .unwrap();

    // Print answers
    for record in response.answers() {
        if let Some(txt) = record.rdata().as_txt() {
            println!("{}", txt.txt_data().join(""));
        }
    }
}

Node.js (dohjs)

const doh = require('dohjs');

const resolver = new doh.DohResolver('https://api.resolvedb.io/dns-query');

async function query() {
  const response = await resolver.query('get.newyork.weather.public.v1.resolvedb.net', 'TXT');

  for (const answer of response.answers) {
    console.log(answer.data);
  }
}

query();

Manual Query Construction

DNS wire format follows RFC 1035. Here's the structure:

Query Structure

+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      ID                       |  2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |  2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    QDCOUNT                    |  2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ANCOUNT                    |  2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    NSCOUNT                    |  2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ARCOUNT                    |  2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                   QUESTION                    |  variable
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

Encoding Labels

Each label is length-prefixed:

get.newyork.weather.public.v1.resolvedb.net

Encodes to:
03 67 65 74     = length(3) + "get"
07 77 65 61 74 68 65 72  = length(7) + "weather"
06 70 75 62 6c 69 63     = length(6) + "public"
02 76 31        = length(2) + "v1"
09 72 65 73 6f 6c 76 65 64 62  = length(9) + "resolvedb"
03 6e 65 74     = length(3) + "net"
00              = null terminator

Base64url Encoding

  1. Remove = padding
  2. Replace + with -
  3. Replace / with _
function base64urlEncode(buffer) {
  return Buffer.from(buffer)
    .toString('base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
}

System Resolver Configuration

Firefox

  1. Open about:config
  2. Set network.trr.mode to 3 (DoH only)
  3. Set network.trr.uri to https://api.resolvedb.io/dns-query

Chrome

  1. Open chrome://settings/security
  2. Enable "Use secure DNS"
  3. Select "With Custom" and enter https://api.resolvedb.io/dns-query

macOS (Encrypted DNS Profile)

Create a .mobileconfig profile:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>PayloadContent</key>
    <array>
        <dict>
            <key>DNSSettings</key>
            <dict>
                <key>DNSProtocol</key>
                <string>HTTPS</string>
                <key>ServerURL</key>
                <string>https://api.resolvedb.io/dns-query</string>
            </dict>
            <key>PayloadType</key>
            <string>com.apple.dnsSettings.managed</string>
            <key>PayloadIdentifier</key>
            <string>io.resolvedb.dns</string>
            <key>PayloadUUID</key>
            <string>A1B2C3D4-E5F6-7890-ABCD-EF1234567890</string>
            <key>PayloadVersion</key>
            <integer>1</integer>
        </dict>
    </array>
    <key>PayloadIdentifier</key>
    <string>io.resolvedb.dns.profile</string>
    <key>PayloadType</key>
    <string>Configuration</string>
    <key>PayloadUUID</key>
    <string>12345678-90AB-CDEF-1234-567890ABCDEF</string>
    <key>PayloadVersion</key>
    <integer>1</integer>
</dict>
</plist>

Linux (systemd-resolved)

Edit /etc/systemd/resolved.conf:

[Resolve]
DNS=api.resolvedb.io
DNSOverTLS=yes

Restart: sudo systemctl restart systemd-resolved

Error Handling

HTTP Errors

CodeDescription
200Success
400Invalid query format or size exceeded
415Unsupported media type
429Rate limit exceeded
500Server error

DNS Errors

Check the RCODE in the response header:

RCODENameDescription
0NOERRORSuccess
1FORMERRMalformed query
2SERVFAILServer failure
3NXDOMAINName does not exist
5REFUSEDQuery refused

Size Limits

LimitValue
GET query parameter8KB (base64 encoded)
POST body4KB
Response64KB

For responses exceeding UDP limits (512 bytes), the response is delivered without truncation over HTTPS.

Rate Limits

Same as JSON API:

PlanRequests/min
Free60
Pro600
EnterpriseCustom

Next Steps