mirror of
https://github.com/0xERR0R/blocky.git
synced 2026-10-12 02:01:13 +00:00
136 lines
5.5 KiB
Go
136 lines
5.5 KiB
Go
package server
|
|
|
|
import (
|
|
"github.com/0xERR0R/blocky/model"
|
|
"github.com/0xERR0R/blocky/util"
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// clientQuery holds the properties of a query as the client itself sent it.
|
|
//
|
|
// The resolver chain mutates request.Req in place: ECS, DNSSEC and the upstream EDNS0 buffer floor
|
|
// may add or enlarge an OPT record the client never sent, and the DNSSEC resolver overwrites the DO
|
|
// bit because a validating resolver must always query upstream with DO set (RFC 4035 §3.2.1). The
|
|
// properties the response has to be normalized against are therefore captured before resolution
|
|
// starts, and applied afterwards by normalizeResponse.
|
|
type clientQuery struct {
|
|
// maxResponseSize is the largest response the client accepts, derived from its own EDNS0
|
|
// buffer size rather than the buffer the chain advertised upstream.
|
|
maxResponseSize int
|
|
|
|
// hadEdns0 records whether the query carried an OPT record.
|
|
hadEdns0 bool
|
|
|
|
// wantsDNSSEC is the state of the EDNS0 DO bit in the query.
|
|
wantsDNSSEC bool
|
|
|
|
// wantsAD records whether the client signalled interest in the AD bit, by setting AD in the
|
|
// query (RFC 6840 §5.7) or by setting DO, which counts as the same signal (§5.8).
|
|
wantsAD bool
|
|
|
|
// recursionDesired is the state of the RD bit in the query.
|
|
recursionDesired bool
|
|
|
|
// qType is the QTYPE of the query, which drives the "explicitly requested" exception when
|
|
// DNSSEC records are stripped. Zero when the query carries no question at all.
|
|
qType uint16
|
|
}
|
|
|
|
func newClientQuery(request *model.Request) clientQuery {
|
|
opt := request.Req.IsEdns0()
|
|
do := opt != nil && opt.Do()
|
|
|
|
// like every other resolver in the chain, only the first question is considered
|
|
var qType uint16
|
|
if len(request.Req.Question) > 0 {
|
|
qType = request.Req.Question[0].Qtype
|
|
}
|
|
|
|
return clientQuery{
|
|
maxResponseSize: getMaxResponseSize(request),
|
|
hadEdns0: opt != nil,
|
|
wantsDNSSEC: do,
|
|
wantsAD: do || request.Req.AuthenticatedData,
|
|
recursionDesired: request.Req.RecursionDesired,
|
|
qType: qType,
|
|
}
|
|
}
|
|
|
|
// normalizeResponse adapts res to the query the client actually sent. It is the last step before
|
|
// the response goes on the wire.
|
|
func (q clientQuery) normalizeResponse(res *dns.Msg) {
|
|
res.RecursionAvailable = q.recursionDesired
|
|
|
|
if !q.wantsAD {
|
|
// RFC 6840 §5.8: only report authenticated data to a client that asked for it by setting
|
|
// DO or AD. The DNSSEC resolver sets AD on every response it validates, without knowing
|
|
// what the client asked for.
|
|
res.AuthenticatedData = false
|
|
}
|
|
|
|
if !q.wantsDNSSEC {
|
|
// RFC 4035 §3.2.1: the DNSSEC records the chain requested upstream on the client's behalf
|
|
// are for us to validate with, not for the client to receive. Stub resolvers may reject a
|
|
// response carrying RRs they never asked for. Runs before Truncate, so stripping them can
|
|
// keep a signed answer under the client's buffer size instead of truncating it.
|
|
util.StripDNSSECRecords(res, q.qType)
|
|
}
|
|
|
|
if !q.hadEdns0 {
|
|
// don't return an OPT record to a client that didn't use EDNS0 (RFC 6891 section 7)
|
|
util.RemoveEdns0Record(res)
|
|
} else if opt := res.IsEdns0(); opt != nil {
|
|
// Blocky doesn't implement DNS Cookies (RFC 7873), so a Server Cookie in the response is
|
|
// one an upstream issued for blocky itself, and blocky can't validate it when the client
|
|
// returns it. Passing it on also makes the presence of a cookie depend on which upstream
|
|
// answered and on whether the answer came from the cache (stored without an OPT record),
|
|
// and a client that tracks cookie support per server address — c-ares does — discards the
|
|
// cookieless answers of such a flip-flopping server as spoofed.
|
|
util.RemoveEdns0OptionKeepRecord[*dns.EDNS0_COOKIE](res)
|
|
|
|
// RFC 3225 §3: the DO bit of the query is copied into the response
|
|
opt.SetDo(q.wantsDNSSEC)
|
|
|
|
// Not every OPT reaching this point is the upstream's: util.SetEdns0Option, which the EDE
|
|
// resolver uses to annotate cache hits and blocked answers, builds one without a class
|
|
// field. RFC 6891 §6.2.4 makes a peer read that zero as 512 and shrink its buffer to match.
|
|
if opt.UDPSize() == 0 {
|
|
opt.SetUDPSize(dns.DefaultMsgSize)
|
|
}
|
|
} else {
|
|
// RFC 6891 §6.1.1: a response to an EDNS0 query must carry an OPT record. Cache hits
|
|
// are served from bytes packed without one; resolvers such as systemd-resolved read
|
|
// its absence as a server without EDNS0 support and downgrade.
|
|
res.SetEdns0(dns.DefaultMsgSize, q.wantsDNSSEC)
|
|
}
|
|
|
|
// truncate if necessary; Truncate also disables compression when the message already fits
|
|
// uncompressed and enables it when compression is needed to fit, so we let it decide rather
|
|
// than forcing Compress=true and paying a compression-map alloc + packing on every response.
|
|
res.Truncate(q.maxResponseSize)
|
|
|
|
// Fitting the buffer the client advertised isn't enough on its own: that client may be a
|
|
// forwarder relaying our answer to a stub with a smaller buffer, and it can only truncate
|
|
// what it received. Compress anything that wouldn't fit a bare 512-byte UDP message so it
|
|
// survives that hop; below that there is nothing to gain and the compression map costs more
|
|
// than the bytes it would save.
|
|
if !res.Compress && res.Len() > dns.MinMsgSize {
|
|
res.Compress = true
|
|
}
|
|
}
|
|
|
|
// For TCP returns 64k
|
|
// For UDP returns EDNS UDP size or if not present, 512
|
|
func getMaxResponseSize(req *model.Request) int {
|
|
if req.Protocol == model.RequestProtocolTCP {
|
|
return dns.MaxMsgSize
|
|
}
|
|
|
|
edns := req.Req.IsEdns0()
|
|
if edns != nil && edns.UDPSize() > 0 {
|
|
return int(edns.UDPSize())
|
|
}
|
|
|
|
return dns.MinMsgSize
|
|
}
|