Repository navigation
DNSCrypt over TCP: ~20% of queries to Quad9 (dnsdist) are dropped because TCP query padding can exceed 256 bytes #3368
Description
Diagnostic output
$ ./dnscrypt-proxy -version
2.1.18
$ ./dnscrypt-proxy -check
[NOTICE] Using default Weighted Power of Two (WP2) load balancing strategy
[NOTICE] Source [public-resolvers] loaded
[NOTICE] Configuration successfully checked
$ ./dnscrypt-proxy -resolve example.com
Resolving [example.com] using 127.0.0.1 port 5053
Resolver : 109.200.199.201 (hosted-by.i3d.net.)
Canonical name: example.com.
IPv4 addresses: 104.20.23.154, 172.66.147.243
...
DNSSEC signed : yes
(official linux_arm64 binary, minisign-verified; only quad9-dnscrypt-ip4-filter-pri and quad9-dnscrypt-ip6-filter-pri in server_names)
- I read the documentation and this is not answered there
- Initially raised as discussion: no, filing directly because the cause is identified below
What's happening?
When a client queries dnscrypt-proxy over TCP, roughly one query in five sent to Quad9 gets no answer. Quad9 accepts the connection, ACKs the query, then closes the connection without sending a response. dnscrypt-proxy then closes the client connection with no reply. Queries over UDP are not affected.
The cause appears to be the size of the TCP query padding:
- In
encrypt()(crypto.go), TCP queries get a random0..255added tominQuestionSize, and the result is then rounded up to a multiple of 64. The padding (including the0x80delimiter) can therefore reach about 318 bytes. This code is the same from 2.1.0 to current master. - dnsdist, which Quad9 runs, drops DNSCrypt TCP queries whose padding exceeds 256 bytes (
pdns/dnsdistdist/dnscrypt.cc):#define DNSCRYPT_MAX_TCP_PADDING_SIZE (256) ... if (tcp && paddingLen > DNSCRYPT_MAX_TCP_PADDING_SIZE) { /* "Dropping encrypted query with too long padding size" */
- The former DNSCRYPT-V2-PROTOCOL.txt (§5) said: "The length of is randomly chosen between 1 and 256 bytes (including the leading 0x80)", and the Internet-Draft kept a 1-256 byte range through its July 2026 revision. dnsdist enforces that limit. The 2026-09-23 revision of the draft now asks receivers to accept any valid padding length, but deployed dnsdist servers do not do that yet.
Evidence (tcpdump on the uplink, unique random names, one query per TCP connection to the local proxy)
Size = TCP payload sent to Quad9 (2-byte length prefix + encrypted query). Padding = plaintext padding including 0x80.
| query names | size sent | padding | result |
|---|---|---|---|
| short (46-byte DNS query) | 130 / 194 / 258 / 322 | ≤ 206 | 44/44 answered |
| short (46-byte DNS query) | 386 | 270 | 16/16 dropped (<ACK then <FIN, no data) |
| long (202-byte DNS query) | 386 / 450 / 514 | ≤ 242 | 52/52 answered |
| long (202-byte DNS query) | 578 | 306 | 10/10 dropped |
A dropped connection on the wire: >SYN <SYN-ACK >ACK >PSH/386 <ACK <FIN >FIN <ACK.
An answered one: >SYN <SYN-ACK >ACK >PSH/322 <ACK <PSH/424 >ACK >FIN <FIN >ACK.
So the failure depends on the padding length, not on the packet size. With typical short names, about 20% of TCP queries are affected (r ≥ ~206 out of 0..255).
When does this occur?
Every time a client uses TCP (for example a Windows host, or dnsmasq/Pi-hole forwarding queries it received over TCP) and the server runs dnsdist. Pi-hole logs it as Connection error (127.0.0.1#5053): TCP connection failed while receiving payload length from upstream (Connection prematurely closed by remote server).
How do we replicate the issue?
Config: listen_addresses = ['127.0.0.1:5053'], server_names = ['quad9-dnscrypt-ip4-filter-pri'], cache = false. Then:
import os, socket, struct
fail = 0
for _ in range(100):
name = f"t-{os.urandom(5).hex()}.wikipedia.org"
qname = b"".join(bytes([len(l)]) + l.encode() for l in name.split(".")) + b"\0"
q = os.urandom(2) + struct.pack(">HHHHH", 0x0100, 1, 0, 0, 0) + qname + struct.pack(">HH", 1, 1)
s = socket.create_connection(("127.0.0.1", 5053), timeout=6)
s.sendall(struct.pack(">H", len(q)) + q)
fail += s.recv(2) == b""
s.close()
print(f"{fail}/100 TCP queries got no answer") # ~20; the same test over UDP: 0Expected behavior
TCP queries are answered like UDP ones. Keeping the TCP query padding within 256 bytes, as earlier versions of the spec described, would presumably avoid the problem.
Additional context
Related but different: #1169 (2020, a dnsdist padding bug that was fixed back then).
Activity
TCP padding must stay within 1..256, including the 0x80 delimiter