* fix(server): always answer an EDNS0 query with an OPT record
Cache hits are served from bytes packed without the OPT record, so a
client that sent one got none back. RFC 6891 §6.1.1 requires an OPT in
the response to any EDNS0 query. systemd-resolved reads its absence as a
server without EDNS0 support, downgrades, and re-probes every ~45 min;
lookups inside a probe window fail with SERVFAIL.
* fix(server): repair the zero payload size on a self-added OPT record
util.SetEdns0Option builds an OPT record with only Name and Rrtype set,
leaving the class field - the UDP payload size - at zero. The EDE resolver
uses it to annotate cache hits, blocked answers, custom DNS and hosts file
responses, so with ede.enable those responses already carry an OPT and take
the mirroring branch, where only the DO bit was set.
RFC 6891 6.2.4 makes a peer read a zero payload size as 512, so blocky
advertised a 512 byte receive buffer while its listener accepts 65535, and
the missing OPT this branch set out to add was never missing there in the
first place.
Add a DO=false spec for the synthesized OPT as well: the existing specs for
a cleared DO bit all use a chain that puts an OPT on the response, so they
exercise the mirroring branch and leave the new one uncovered.
Claude-Session: https://claude.ai/code/session_01JKQa9d3Uiwu5JfJwEfVC82
---------
Co-authored-by: Dimitri Herzog <[email protected]>
* fix(server): compress responses larger than 512 bytes
Since #2102 the server lets Truncate decide compression, and Truncate
clears Compress whenever the message already fits uncompressed. Fitting
the buffer the client advertised is not sufficient: that client may be a
forwarder relaying our answer to a stub resolver with a smaller buffer,
and it can only truncate what it received.
A query arriving via such a forwarder carries the forwarder's OPT record,
so getMaxResponseSize returns its buffer (1232 or more) rather than 512.
An eight-record A answer for a 45-byte name is 562 bytes uncompressed and
202 compressed: both fit 1232, so we now send the uncompressed one, and
the forwarder has to truncate it for its non-EDNS0 clients -- typically by
dropping every section, leaving them with TC and no answer at all. Before
#2102 the same answer went out compressed, fit 512, and was relayed intact.
Compress anything that would not fit a bare 512-byte UDP message so it
survives that hop. Below that threshold there is nothing to gain, so the
compression-map alloc #2102 set out to avoid is still skipped for the
short responses that make up the bulk of the traffic.
Fixes#2212
* test(server): guard that the compression regression test stays meaningful
Assert the answer really exceeds 512 bytes uncompressed, so shrinking the
RR set can't leave the test passing without exercising the scenario.
* refactor(server): group the client-facing response normalization
The response the server writes back is normalized against the query as the
client sent it, not against request.Req, which the resolver chain mutates in
place. That was two captured locals and three statements spread across
resolve(); collect them in a clientQuery value captured before resolution and
applied afterwards, so further per-client normalization has one home.
No behavior change: the existing server specs pass unchanged.
* fix(dnssec): don't return DNSSEC records to clients with the DO bit clear
Fixes#2202.
A validating resolver must query upstream with the DO bit set, so the DNSSEC
resolver sets it on every request regardless of what the client asked for. The
records that come back were then forwarded verbatim, so a client that left DO
clear received the RRSIG (and NSEC/NSEC3/DNSKEY/DS) records blocky had fetched
for its own validation.
Normalize the response against the client's DO bit in clientQuery:
- strip the DNSSEC records, keeping any type the query explicitly asked for, so
a DNSKEY or DS query is still answered, and keeping everything for an ANY
query (RFC 4035 section 3.2.1, RFC 3225 section 3)
- copy the query's DO bit into the response's OPT record (RFC 3225 section 3)
- clear AD unless the client set DO or AD (RFC 6840 section 5.8)
Stripping runs before Truncate, so a signed answer that only exceeded the
client's buffer because of the signatures is no longer truncated.
This sits in the server rather than in the DNSSEC resolver because the cache is
keyed on qtype and name only: an entry populated by a DO client is served to
every other client of that name, so the records have to be stripped per client
even when dnssec.validate is off and the DNSSEC resolver is a no-op.
Not addressed: the reverse of that cache behaviour, where a DO=1 client is
served an unsigned entry populated by a DO=0 client (#1741). That needs a
cache-key change; with dnssec.validate enabled it cannot happen, because the
chain always fetches with DO set.
---------
Co-authored-by: Dimitri Herzog <[email protected]>
Client name resolution previously only used the static clientLookup.clients
mapping or a reverse-DNS query against a configured clientLookup.upstream.
With neither set, the client IP was used as its own name even when Blocky
already knew the name from a hosts file or custom DNS entry.
ClientNamesResolver now consults Blocky's in-memory reverse sources
(custom DNS, hosts files) before the rDNS upstream, performing no network
round-trip. Resolution order: static mapping -> local sources -> rDNS
upstream -> IP fallback. This also makes the per-client CSV log filename
use the resolved hostname.
Fix TCP response truncation
Currently a TCP response is truncated to the maximum size advertised for
UDP. This means that a client cannot effectively fallback to TCP when
receiving at truncated UDP response, as the resulting TCP response is
also truncated.
This change ensures that only UDP responses are truncated
Fixed a nil pointer dereference panic that occurred when
CustomDNSResolver returned a CNAME record pointing to an external
domain. The bug was caused by RewriterResolver's branching architecture
isolating resolvers with NoOpResolver, preventing proper CNAME chain
resolution.
* feat(config): read read header timeout value
This commit adds a request header read timeout configuration parameter.
As it is part of the downloading cycle, it is available under
`blocking/loading/downloads`.
The default value comes from `server/http.go` line 21.
Refs: #1653
* feat(server): get read header timeout from config
* feat(docs): document additional config parameter
* feat(config): read read timeout value
This commit adds a request read timeout configuration parameter. As it
is part of the downloading cycle, it is available under
`blocking/loading/downloads`.
The default value comes from `server/http.go` line 22.
Refs: #1653
* feat(server): get read timeout from config
* feat(docs): document additional config parameter
* refactor(server): remove superfluous spaces
* feat(config): read write timeout value
This commit adds a write timeout configuration parameter. As it is part
of the downloading cycle, it is available under
`blocking/loading/downloads`.
The default value comes from `server/http.go` line 23.
Refs: #1606
* feat(server): get write timeout from config
Refs: #1606
* feat(docs): document additional config parameter
---------
Co-authored-by: Dimitri Herzog <[email protected]>
Remove that code from util and the use of globals.
Partly move away from calling the value a "hostname" since it's more of
an instance ID, especially if you run more than one per host.