git » dnss » commit 900fbff

dnsserver: Truncate UDP responses that are too large

author Alberto Bertogli
2021-06-12 10:57:26 UTC
committer Alberto Bertogli
2021-06-12 14:21:40 UTC
parent b5631eac24e6cbfddd36fdbfe01ad085d50a7afe

dnsserver: Truncate UDP responses that are too large

When an UDP response is larger than accepted by the client (defaults to
512, but can be extended using EDNS0), the standard says we should
truncate it.

Currently, we don't. In practice, this often works anyways because most
client libraries don't mind, but it can cause problems if the reply
exceeds the UDP packet size, and/or if the client is strict in checking
(like some Go libraries are).

This patch makes dnss conform to the standard by truncating DNS
responses over UDP as needed.

In those cases, the client is expected to retry over TCP (which doesn't
have this issue).

Note that unfortunately cleanbrowsing.org DoH resolver has to be removed
from the tests, because they don't handle this case correctly, and fail
all queries with large responses.

internal/dnsserver/resolver.go +2 -0
internal/dnsserver/server.go +19 -2
tests/external.sh +28 -1

diff --git a/internal/dnsserver/resolver.go b/internal/dnsserver/resolver.go
index 73666cb..86db83d 100644
--- a/internal/dnsserver/resolver.go
+++ b/internal/dnsserver/resolver.go
@@ -194,6 +194,8 @@ func wantToCache(question dns.Question, reply *dns.Msg) error {
 		return fmt.Errorf("answer is empty")
 	} else if len(reply.Question) != 1 {
 		return fmt.Errorf("too many/few questions (%d)", len(reply.Question))
+	} else if reply.Truncated {
+		return fmt.Errorf("truncated reply")
 	} else if reply.Question[0] != question {
 		return fmt.Errorf(
 			"reply question does not match: asked %v, got %v",
diff --git a/internal/dnsserver/server.go b/internal/dnsserver/server.go
index 71d61f6..e26c6dd 100644
--- a/internal/dnsserver/server.go
+++ b/internal/dnsserver/server.go
@@ -86,7 +86,7 @@ func (s *Server) Handler(w dns.ResponseWriter, r *dns.Msg) {
 		if err == nil {
 			tr.Printf("used unqualified upstream")
 			tr.Answer(u)
-			w.WriteMsg(u)
+			s.writeReply(tr, w, r, u)
 		} else {
 			tr.Printf("unqualified upstream error: %v", err)
 			dns.HandleFailed(w, r)
@@ -113,7 +113,24 @@ func (s *Server) Handler(w dns.ResponseWriter, r *dns.Msg) {
 	tr.Answer(fromUp)
 
 	fromUp.Id = oldid
-	w.WriteMsg(fromUp)
+	s.writeReply(tr, w, r, fromUp)
+}
+
+func (s *Server) writeReply(tr *trace.Trace, w dns.ResponseWriter, r, reply *dns.Msg) {
+	if w.RemoteAddr().Network() == "udp" {
+		// We need to check if the response fits.
+		// UDP by default has a maximum of 512 bytes. This can be extended via
+		// the client in the EDNS0 record.
+		max := 512
+		ednsOPT := r.IsEdns0()
+		if ednsOPT != nil {
+			max = int(ednsOPT.UDPSize())
+		}
+		reply.Truncate(max)
+		tr.Printf("UDP max:%d truncated:%v", max, reply.Truncated)
+	}
+
+	w.WriteMsg(reply)
 }
 
 // ListenAndServe launches the DNS proxy.
diff --git a/tests/external.sh b/tests/external.sh
index 2d21def..b7dab5b 100755
--- a/tests/external.sh
+++ b/tests/external.sh
@@ -87,6 +87,34 @@ function resolve() {
 		cat .dig.log
 		false
 	fi
+
+	# The response exceeds the default UDP size (512b), so it should fall back
+	# to TCP. This exercises the truncating logic.
+	kdig @127.0.0.1:1053  google.com TXT > .dig.log 2>&1
+	if ! grep -E -i -q '^google.com.*TXT'  .dig.log; then
+		echo "----- FAILED (missing response)"
+		cat .dig.log
+		false
+	fi
+	if ! grep -E -i -q 'retrying over TCP'  .dig.log; then
+		echo "----- FAILED (did not use TCP)"
+		cat .dig.log
+		false
+	fi
+
+	# Same as above, but we explicitly are ok with a 2k response, which
+	# fits the reply. We check that we do NOT fall back to TCP.
+	kdig @127.0.0.1:1053 +bufsize=2048 google.com TXT > .dig.log 2>&1
+	if ! grep -E -i -q '^google.com.*TXT'  .dig.log; then
+		echo "----- FAILED (missing response)"
+		cat .dig.log
+		false
+	fi
+	if grep -E -i -q 'retrying over TCP'  .dig.log; then
+		echo "----- FAILED (used TCP)"
+		cat .dig.log
+		false
+	fi
 }
 
 # HTTP GET, using wget.
@@ -190,7 +218,6 @@ for server in \
 	"https://cloudflare-dns.com/dns-query" \
 	"https://dns.google/dns-query" \
 	"https://dns.quad9.net/dns-query" \
-	"https://doh.cleanbrowsing.org/doh/family-filter/" \
 	"https://doh.powerdns.org" \
 	;
 do