Introduction
DNS over QUIC (DoQ, RFC 9250) combines DNS encryption with QUIC transport — a UDP-based protocol that eliminates head-of-line blocking and supports native connection migration. DoQ provides three advantages over DoH and DoT: 0-RTT connection resumption (repeat queries skip the handshake), connection migration (queries survive WiFi-to-cellular handoff), and no head-of-line blocking (each query is an independent QUIC stream). Standardized in 2022 and widely deployed by 2025-2026, DoQ is now supported by major DNS providers including Cloudflare, Google, Quad9 (global rollout April 2026), and AdGuard.
This guide covers the QUIC handshake and DoQ resolution flow with Mermaid diagrams, server-side deployment with Unbound and Technitium DNS Server, a Python client using aioquic for sending DNS queries over QUIC, and troubleshooting common deployment issues.
QUIC Handshake and DoQ Resolution
QUIC combines the TLS 1.3 handshake with transport establishment in a single round trip, or zero for resumed connections:
sequenceDiagram
participant C as Client
participant S as DoQ Server
Note over C,S: Initial Connection (1-RTT)
C->>S: Initial: ClientHello + QUIC Transport Params
S-->>C: Handshake: ServerHello + Certificate + DNS Stream Ready
C->>S: 1-RTT: DNS Query (stream ID 0)
S-->>C: 1-RTT: DNS Response (stream ID 0)
Note over C,S: Resumed Connection (0-RTT!)
C->>S: 0-RTT: DNS Query (stream ID 0) + ClientHello
S-->>C: Handshake + DNS Response
Note over C,S: Connection Migration
C->>S: [Switched from WiFi to Cellular]
C->>S: DNS Query (new path, same connection ID)
S-->>C: DNS Response (connection maintained)
Protocol Comparison
| Feature | Traditional DNS | DoT (TLS) | DoH (HTTPS) | DoQ (QUIC) |
|---|---|---|---|---|
| Port | UDP 53 | TCP 853 | TCP 443 | UDP 853 |
| Handshake | None | TCP + TLS (2 RTT) | TCP + TLS (2 RTT) | QUIC (1 RTT initial, 0 RTT resume) |
| Head-of-line blocking | No | Yes (TCP) | Yes (TCP) | No (independent streams) |
| Connection migration | N/A | No | No | Yes |
| Firewall bypass | Blocked easily | Port 853 visible | Hidden in HTTPS | Port 853 UDP |
| Standard | RFC 1035 | RFC 7858 | RFC 8484 | RFC 9250 |
Server-Side Deployment
DoQ server support has matured significantly in 2025-2026. Three major open-source DNS servers now support it.
Unbound
Unbound 1.22.0+ includes native DoQ support. Enable it in unbound.conf:
server:
interface: 0.0.0.0
port: 53
do-ip4: yes
do-udp: yes
do-tcp: yes
# QUIC settings
quic-port: 853
do-quic: yes
# TLS certificate (same cert works for DoT and DoQ)
tls-service-key: /etc/unbound/unbound_server.key
tls-service-pem: /etc/unbound/unbound_server.pem
# Access control
access-control: 10.0.0.0/8 allow
access-control: 127.0.0.0/8 allow
Restart and verify:
sudo systemctl restart unbound
sudo ss -ulpn | grep 853
# udp UNCONN 0 0 0.0.0.0:853 0.0.0.0:* users:(("unbound",pid=1234))
Technitium DNS Server
Technitium DNS Server is an open-source C# DNS server with DoT, DoH, and DoQ support including zone transfers over QUIC:
# Download and install (Linux)
wget https://github.com/TechnitiumSoftware/DnsServer/releases/latest/download/TechnitiumDnsServer_amd64.deb
sudo dpkg -i TechnitiumDnsServer_amd64.deb
# Enable DoQ via web admin console (port 5380)
# Settings → DNS Server → Enable DNS over QUIC (DoQ) → Port 853
Verify with q (a tiny DoQ CLI client):
q example.com @quic://dns.technitium.net --tls-no-verify
# example.com. 300 A 93.184.216.34
AdGuard Home
AdGuard Home supports DoQ both as a client (forwarding to upstream DoQ servers) and as a server (accepting DoQ queries from clients). Enable upstream DoQ in the settings:
upstream_dns:
- quic://dns.adguard.com
- quic://dns.quad9.net
- quic://1.1.1.1:853
Python DoQ Client
A functional DoQ client using aioquic and dnspython. Unlike DoH which uses HTTPS POST, DoQ sends DNS wire format directly in QUIC streams — no HTTP layer involved:
import asyncio
import ssl
import dns.message
import dns.rdatatype
from aioquic.asyncio import connect
from aioquic.asyncio.protocol import QuicConnectionProtocol
from aioquic.quic.configuration import QuicConfiguration
from aioquic.quic.events import StreamDataReceived
class DoqProtocol(QuicConnectionProtocol):
"""QUIC protocol that captures the first DNS response on any stream."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.response_future: asyncio.Future[bytes] | None = None
def quic_event_received(self, event):
if isinstance(event, StreamDataReceived):
if self.response_future and not self.response_future.done():
self.response_future.set_result(event.data)
async def resolve(
server_host: str = "1.1.1.1",
domain: str = "example.com",
record_type: str = "A",
server_port: int = 853,
timeout: float = 10.0,
) -> list[str]:
"""Resolve a DNS record via DoQ (RFC 9250).
Connects to a DoQ server over QUIC (UDP port 853),
sends a DNS wire-format query on a new stream,
and parses the response without HTTP encapsulation.
"""
query = dns.message.make_query(domain, dns.rdatatype.from_text(record_type))
wire_query = query.to_wire()
config = QuicConfiguration(
is_client=True,
alpn_protocols=["doq"],
verify_mode=ssl.CERT_REQUIRED,
server_name=server_host,
)
transport_protocol = DoqProtocol
async with connect(
server_host,
server_port,
configuration=config,
create_protocol=transport_protocol,
) as client:
client.response_future = asyncio.get_event_loop().create_future()
stream_id = client._quic.get_next_available_stream_id()
client._quic.send_stream_data(stream_id, wire_query, end_stream=True)
client.transmit()
response_data = await asyncio.wait_for(
client.response_future, timeout=timeout
)
answer = dns.message.from_wire(response_data)
return [str(rrset[0]) for rrset in answer.answer]
async def main():
result = await resolve("1.1.1.1", "example.com", "A")
print(f"example.com A records: {result}")
result6 = await resolve("1.1.1.1", "example.com", "AAAA")
print(f"example.com AAAA records: {result6}")
result_mx = await resolve("1.1.1.1", "example.com", "MX")
print(f"example.com MX records: {result_mx}")
if __name__ == "__main__":
asyncio.run(main())
Install dependencies:
pip install aioquic dnspython
python doq_client.py
# example.com A records: ['93.184.216.34']
# example.com AAAA records: ['2606:2800:21f:cb07:6820:80da:af6b:8b2c']
# example.com MX records: ['0 .']
Testing and Troubleshooting
Command-Line Tools
Several CLI tools support DoQ queries for quick testing:
# Install q (Go-based DoQ/D DoH/D DoT client)
go install github.com/natesales/q@latest
# Query Cloudflare DoQ
q example.com @quic://1.1.1.1
# Query Quad9 DoQ
q example.com @quic://dns.quad9.net
# Query AdGuard DoQ
q example.com @quic://dns.adguard.com --tls-no-verify
# With specific record type
q example.com AAAA @quic://1.1.1.1
Common Issues
UDP receive buffer size — QUIC requires larger UDP receive buffers than typical DNS. The q client warns when the buffer is too small:
# Check current buffer size
sysctl net.core.rmem_max
# Increase permanently (requires root)
sudo sysctl -w net.core.rmem_max=26214400
sudo sysctl -w net.core.wmem_max=26214400
Firewall rules — DoQ uses UDP 853, not TCP 853 like DoT. Ensure your firewall allows it:
# Allow DoQ on server
sudo ufw allow 853/udp
# Verify the rule
sudo ufw status | grep 853
# 853/udp ALLOW Anywhere
ALPN mismatch — Servers expect the doq ALPN token (not h3 or hq). Verify with Wireshark or tcpdump:
sudo tcpdump -i any -nn port 853 -X
# Look for ALPN negotiation in the QUIC handshake
Verification Checklist
# 1. Check if the server responds on UDP 853
nc -zu -w 3 1.1.1.1 853 && echo "Port open" || echo "Port closed or filtered"
# 2. Verify DoQ resolution works
q example.com @quic://1.1.1.1
# 3. Compare latency against DoT
time q example.com @tls://1.1.1.1
time q example.com @quic://1.1.1.1
# 4. Monitor QUIC connection stats
ss -ulnp sport = :853
Performance Characteristics
| Metric | DoQ | DoH | DoT | Traditional DNS |
|---|---|---|---|---|
| First query latency | ~RTT + 0ms (1-RTT handshake) | ~RTT + 200ms (TCP+TLS) | ~RTT + 200ms | ~RTT (no encryption) |
| Repeat query latency | ~0ms (0-RTT) | ~RTT + 200ms | ~RTT + 200ms | ~RTT |
| Connection migration | Supported | No | No | N/A |
| Overhead per query | 0 (stream in existing conn) | HTTP headers (~300 bytes) | TLS record | 0 |
| Packet loss recovery | Per-stream (isolated) | Per-connection (TCP) | Per-connection (TCP) | Single packet |
| Best for | Mobile users, privacy-sensitive | General encrypted DNS | Dedicated DNS infrastructure | Legacy, LAN-only |
DoQ’s 0-RTT resumption is its strongest advantage for mobile users who frequently reconnect. A phone switching between WiFi and cellular can continue DNS resolution without re-establishing encryption — the QUIC connection ID persists across network changes.
In high-loss environments (packet loss >1%), DoQ maintains stable resolution times while TCP-based DoT and DoH degrade significantly due to TCP’s head-of-line blocking. A 2026 IEEE measurement study confirmed DoQ’s latency advantage in realistic mobile and lossy network conditions.
Use Cases
Mobile and Roaming
Mobile clients benefit most from DoQ. When a device moves between WiFi and cellular, QUIC connection migration keeps the DNS session alive — no new handshake, no dropped queries. This contrasts with DoT where every network change triggers a fresh TCP+TLS handshake (~2 RTT penalty).
IoT and Low-Power Devices
QUIC’s 0-RTT resumption reduces the number of round trips for each DNS query, saving battery on resource-constrained IoT devices. Combined with fewer packet exchanges compared to TCP+TLS, DoQ can extend battery life for devices that perform frequent DNS lookups.
Privacy-Conscious Browsing
DoQ on port 853 UDP is harder to block than DoT (port 853 TCP) without collateral damage — many enterprise and cloud services rely on UDP. This makes DoQ attractive for users in restrictive networks, though it provides less camouflage than DoH which hides inside HTTPS traffic on port 443.
Best Practices
- Always validate certificates — Set
verify_mode = ssl.CERT_REQUIREDin production. Self-signed certificates are acceptable for internal resolvers if distributed via your CA. - Combine with DNSSEC — DoQ encrypts the transport but does not validate DNS data integrity. Deploy DNSSEC alongside DoQ for end-to-end security.
- Use connection pooling — Reuse the QUIC connection for multiple queries instead of opening a new connection per query. Each query gets its own stream within the same connection.
- Set appropriate timeouts — QUIC connections are longer-lived than TCP, but idle connections still consume server resources. Configure idle timeouts (default 30s in most implementations).
- Monitor QUIC-specific metrics — Track connection migration events, 0-RTT acceptance rate, and per-stream loss rates alongside standard DNS metrics.
Common Pitfalls
- ALPN misconfiguration — DoQ requires the
doqALPN token. Settingh3or leaving ALPN empty causes handshake failures. Verify with Wireshark. - UDP buffer exhaustion — QUIC servers need larger UDP receive buffers than typical DNS servers. Symptoms include dropped connections and timeouts under load.
- MTU issues — QUIC performs path MTU discovery. If ICMP “fragmentation needed” messages are blocked by firewalls, QUIC connections may stall. Ensure
ICMPv6 Type 2andICMPv4 Type 3 Code 4are permitted. - Load balancer confusion — Traditional TCP load balancers cannot handle QUIC. Use UDP-aware load balancers that support QUIC connection ID routing (not IP+port hashing) to maintain connection migration functionality.
- 0-RTT replay attacks — 0-RTT data can be replayed by an attacker. DoQ servers should implement replay protection as specified in RFC 9001, and clients should not send non-idempotent operations in 0-RTT.
Resources
- RFC 9250 — DNS over Dedicated QUIC Connections
- aioquic Python Library — QUIC and HTTP/3 implementation
- dnspython Documentation — DNS message construction and parsing
- Cloudflare DoQ Support — Provider documentation
- QUIC Transport Protocol (RFC 9000) — Underlying transport
- A Long-Term View of DoQ Adoption (IEEE, 2026) — Measurement study of DoQ adoption and performance
- Unbound DoQ Documentation — Open-source recursive resolver with QUIC support
- Technitium DNS Server — C# DNS server with DoT, DoH, DoQ, and XFR-over-QUIC
Comments