Skip to content

DNSCrypt over TCP: ~20% of queries to Quad9 (dnsdist) are dropped because TCP query padding can exceed 256 bytes #3368

Closed

Description

@fabio-lapolla

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 random 0..255 added to minQuestionSize, and the result is then rounded up to a multiple of 64. The padding (including the 0x80 delimiter) 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: 0

Expected 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

  1. added a commit that references this issue on Oct 15, 2026
    7019609
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions