Skip to content

Commit aeba99b

Browse files
committed
internal/handler: more RFC compliance
This commit adds appropriate error handling when dealing with EDNS0 stuff. Primarily version checking and rejecting anything that does not have a version of zero. On top of this I (with the help of ChatGPT) have attempted to annotate code paths with the RFCs they relate to and listed all the RFCs at the top of the file. Signed-off-by: David Bond <davidsbond93@gmail.com>
1 parent 325a7db commit aeba99b

File tree

1 file changed

+66
-15
lines changed

1 file changed

+66
-15
lines changed

internal/handler/handler.go

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1+
// Package handler provides all handling logic for DNS queries over both standard (raw TCP and UDP) and encrypted
2+
// (DNS-over-TLS, DNS-over-HTTPs) protocols. It includes the allow/block listing logic as well as upstream weighting
3+
// by round-trip-time.
14
package handler
25

6+
// Throughout this package are comments that link specific behavior to DNS-related RFCs. These RFCs can be read at:
7+
// * RFC-1035 (Core DNS): https://www.rfc-editor.org/rfc/rfc1035.html
8+
// * RFC-6891 (EDNS0): https://www.rfc-editor.org/rfc/rfc6891.html
9+
// * RFC-4035 (DNSSEC signaling): https://www.rfc-editor.org/rfc/rfc4035.html
10+
// * RFC-8484 (DNS over HTTPS): https://www.rfc-editor.org/rfc/rfc8484.html
311
import (
412
"context"
513
"encoding/base64"
@@ -49,6 +57,28 @@ func New(allow, block *set.Set[string], upstreams []string, logger *slog.Logger)
4957
}
5058
}
5159

60+
var (
61+
// We need to append this to the "Extra" section of the response whenever we have an EDNS0 version that we
62+
// do not support, so we declare it once here and pass it into Handler.dnsError when required.
63+
badVersionOpt = &dns.OPT{
64+
Hdr: dns.RR_Header{
65+
Name: ".",
66+
Rrtype: dns.TypeOPT,
67+
},
68+
}
69+
)
70+
71+
func init() {
72+
// A little dirty, but we also need to specify the version and set our payload size once for this helpful
73+
// variable.
74+
badVersionOpt.SetVersion(0)
75+
badVersionOpt.SetUDPSize(udpPayloadSize)
76+
}
77+
78+
const (
79+
udpPayloadSize = 1232
80+
)
81+
5282
// ServeDNS handles an inbound DNS request attempting to perform a DNS query using UDP or TCP.
5383
func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
5484
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
@@ -57,15 +87,23 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
5787
response := new(dns.Msg)
5888
response.SetReply(r)
5989

90+
// RFC-6891 (6.1.3): If a responder does not implement the requested EDNS version,it MUST respond with RCODE=BADVERS.
91+
if opt := r.IsEdns0(); opt != nil && opt.Version() != 0 {
92+
h.dnsError(w, r, dns.RcodeBadVers, badVersionOpt)
93+
return
94+
}
95+
96+
// RFC-1035 (4.1.2): While the protocol allows multiple questions per message, most resolvers do not support this
97+
// and return NOTIMP.
6098
if len(r.Question) != 1 {
61-
// Handling multiple questions per request is typically not supported.
6299
h.dnsError(w, r, dns.RcodeNotImplemented)
63100
return
64101
}
65102

66103
question := r.Question[0]
67104
name := strings.TrimSuffix(strings.ToLower(question.Name), ".")
68105

106+
// RFC-1035 (4.3.1): NXDOMAIN indicates that the domain name does not exist. Used here for policy-based blocking.
69107
if h.block.Contains(name) && !h.allow.Contains(name) {
70108
h.dnsError(w, r, dns.RcodeNameError)
71109
return
@@ -86,6 +124,7 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
86124
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
87125
const maxBytes = 4096
88126

127+
// RFC-8484 (4): DNS-over-HTTPS supports both GET and POST methods.
89128
if r.Method != http.MethodPost && r.Method != http.MethodGet {
90129
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
91130
return
@@ -106,6 +145,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
106145
// DNS-over-HTTPs allows both POST and GET requests. If we're using a POST request we'll want to read directly
107146
// from the request body.
108147
if r.Method == http.MethodPost {
148+
// RFC-8484 (4.1): POST requests MUST use Content-Type: application/dns-message.
109149
if r.Header.Get("Content-Type") != "application/dns-message" {
110150
http.Error(w, http.StatusText(http.StatusUnsupportedMediaType), http.StatusUnsupportedMediaType)
111151
return
@@ -134,6 +174,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
134174
return
135175
}
136176

177+
// RFC-8484 (4.1): GET requests encode the DNS message using base64 in the "dns" query parameter.
137178
data, err = base64.RawURLEncoding.DecodeString(query)
138179
if err != nil {
139180
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
@@ -146,11 +187,18 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
146187
return
147188
}
148189

149-
// From this point on, we're always responding with an HTTP 200 and the DNS-encoded messages, so we set the headers
150-
// now for all possible DNS responses.
190+
// RFC-8484 (5): DNS responses are always returned with HTTP 200.
151191
w.Header().Set("Content-Type", "application/dns-message")
152192
w.Header().Set("Cache-Control", "no-store")
153193

194+
// RFC-6891 (6.1.3): If a responder does not implement the requested EDNS version, it MUST respond with BADVERS.
195+
if opt := req.IsEdns0(); opt != nil && opt.Version() != 0 {
196+
h.dnsError(w, req, dns.RcodeBadVers, badVersionOpt)
197+
return
198+
}
199+
200+
// RFC-1035 (4.1.2): While the protocol allows multiple questions per message, most resolvers do not support this
201+
// and return NOTIMP.
154202
if len(req.Question) != 1 {
155203
h.dnsError(w, req, dns.RcodeNotImplemented)
156204
return
@@ -159,6 +207,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
159207
question := req.Question[0]
160208
name := strings.TrimSuffix(strings.ToLower(question.Name), ".")
161209

210+
// RFC-1035 (4.3.1): NXDOMAIN indicates that the domain name does not exist. Used here for policy-based blocking.
162211
if h.block.Contains(name) && !h.allow.Contains(name) {
163212
h.dnsError(w, req, dns.RcodeNameError)
164213
return
@@ -181,11 +230,13 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
181230
}
182231
}
183232

184-
func (h *Handler) dnsError(w io.Writer, r *dns.Msg, code int) {
233+
func (h *Handler) dnsError(w io.Writer, r *dns.Msg, code int, extra ...dns.RR) {
185234
response := new(dns.Msg)
186235
response.SetReply(r)
187236
response.SetRcode(r, code)
188237

238+
response.Extra = append(response.Extra, extra...)
239+
189240
data, err := response.Pack()
190241
if err != nil {
191242
// We panic here as we really should never be in this situation unless something has really gone wrong. There
@@ -200,10 +251,8 @@ func (h *Handler) dnsError(w io.Writer, r *dns.Msg, code int) {
200251
}
201252

202253
func (h *Handler) dnsUpstream(ctx context.Context, r *dns.Msg) (*dns.Msg, error) {
203-
const udpSize = 1232
204-
205-
// Before we upstream a DNS request, we'll cap the UDP size to prevent the upstream from sending large
206-
// or fragmented messages.
254+
// RFC-6891 (6.2.3): EDNS allows clients to advertise a larger UDP payload size. We cap this to 1232 bytes to avoid
255+
// IP fragmentation.
207256
opt := r.IsEdns0()
208257
if opt == nil {
209258
opt = &dns.OPT{
@@ -216,8 +265,8 @@ func (h *Handler) dnsUpstream(ctx context.Context, r *dns.Msg) (*dns.Msg, error)
216265
r.Extra = append(r.Extra, opt)
217266
}
218267

219-
if opt.UDPSize() < udpSize {
220-
opt.SetUDPSize(udpSize)
268+
if opt.UDPSize() < udpPayloadSize {
269+
opt.SetUDPSize(udpPayloadSize)
221270
}
222271

223272
// Iterate over the upstreams in ascending order of most recent round-trip-time.
@@ -230,7 +279,7 @@ func (h *Handler) dnsUpstream(ctx context.Context, r *dns.Msg) (*dns.Msg, error)
230279
continue
231280
}
232281

233-
// The upstream has given us a truncated response, so we need to switch over to TCP to get the full response.
282+
// RFC-1035 (4.2.2): If the TC (truncated) bit is set, the client should retry using TCP.
234283
if resp.Truncated {
235284
resp, _, err = h.tcpClient.ExchangeContext(ctx, r, upstream)
236285
if err != nil {
@@ -253,17 +302,19 @@ func (h *Handler) dnsUpstream(ctx context.Context, r *dns.Msg) (*dns.Msg, error)
253302
continue
254303
}
255304

256-
// We want to avoid sending any EDNS0 options from the upstream to avoid leaking any upstream
257-
// metadata.
305+
// RFC-6891 (6.1.1): EDNS0 options are hop-by-hop and MUST NOT be blindly forwarded. We strip upstream options
306+
// to avoid leaking upstream metadata.
258307
opt = resp.IsEdns0()
259308
if opt != nil {
260309
opt.Option = nil
261310
}
262311

263-
// We need to update the flags to correspond with what this DNS server provides, rather than the
264-
// upstream itself.
312+
// RFC-{1035,4035}: This server is not authoritative but does provide recursion.
265313
resp.Authoritative = false
266314
resp.RecursionAvailable = true
315+
316+
// RFC-4035 (3.2.3): The AD (Authenticated Data) bit MUST only be set by a resolver that has performed DNSSEC
317+
// validation itself.
267318
resp.AuthenticatedData = false
268319

269320
return resp, nil

0 commit comments

Comments
 (0)