Repository: miekg/dns
Pull request #1446 (2023-03-29 01:35:29)

Buffer bump
Also version bump

See individual commit messages

tmthrgd: Do people really care about GPG/PGP these days?
miekg: To add to @tmthrgd reply: this is with older RSA keys or something? Messsages this big will also lead to fragmentation problems on the wire
miekg: [ Quoting ***@***.***> in "Re: [miekg/dns] Buffer bump (PR #14..." ]
>    Do people really care about GPG/PGP these days?
>
>case in point.
>
>    To add to @tmthrgd reply: this is with older RSA keys or something?
>    Messsages this big will also lead to fragmentation problems on the wire
>
>messages this small evidently lead to problems in this lib before it even gets
>on the wire.
>
>your point is moot: DOT, DOH, TCP based protos resolve this. These problems are
>discussed in the RFC.

just upping the value also isn't a real solution. Add a space (as bind also randomly does)
should (i think) also work, as then you get multiple tokens.

all the newer based protos are mostly for the last mile, so of very much limited value

systemcrash: > just upping the value also isn't a real solution. Add a space (as bind also randomly does) should (i think) also work, as then you get multiple tokens. all the newer based protos are mostly for the last mile, so of very much limited value

It's low hanging fruit - the RFC states just a single line of base64 without elaborating further. 
systemcrash: OK - how about this? Just reduce the overall buffer size to 512 bytes, and make it something we can grow by 512 bytes every time we run out. It also fixes me problem, and obsoletes length test cases (or at least reverses the fail logic).

Patch:
diff --git a/client.go b/client.go
index 9051ae00..2cdd49af 100644
--- a/client.go
+++ b/client.go
@@ -6,7 +6,6 @@ import (
 	"context"
 	"crypto/tls"
 	"encoding/binary"
-	"fmt"
 	"io"
 	"net"
 	"strings"
@@ -56,14 +55,20 @@ type Client struct {
 	// Timeout is a cumulative timeout for dial, write and read, defaults to 0 (disabled) - overrides DialTimeout, ReadTimeout,
 	// WriteTimeout when non-zero. Can be overridden with net.Dialer.Timeout (see Client.ExchangeWithDialer and
 	// Client.Dialer) or context.Context.Deadline (see ExchangeContext)
-	Timeout        time.Duration
-	DialTimeout    time.Duration     // net.DialTimeout, defaults to 2 seconds, or net.Dialer.Timeout if expiring earlier - overridden by Timeout when that value is non-zero
-	ReadTimeout    time.Duration     // net.Conn.SetReadTimeout value for connections, defaults to 2 seconds - overridden by Timeout when that value is non-zero
-	WriteTimeout   time.Duration     // net.Conn.SetWriteTimeout value for connections, defaults to 2 seconds - overridden by Timeout when that value is non-zero
-	TsigSecret     map[string]string // secret(s) for Tsig map[<zonename>]<base64 secret>, zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2)
-	TsigProvider   TsigProvider      // An implementation of the TsigProvider interface. If defined it replaces TsigSecret and is used for all TSIG operations.
-	SingleInflight bool              // if true suppress multiple outstanding queries for the same Qname, Qtype and Qclass
-	group          singleflight
+	Timeout      time.Duration
+	DialTimeout  time.Duration     // net.DialTimeout, defaults to 2 seconds, or net.Dialer.Timeout if expiring earlier - overridden by Timeout when that value is non-zero
+	ReadTimeout  time.Duration     // net.Conn.SetReadTimeout value for connections, defaults to 2 seconds - overridden by Timeout when that value is non-zero
+	WriteTimeout time.Duration     // net.Conn.SetWriteTimeout value for connections, defaults to 2 seconds - overridden by Timeout when that value is non-zero
+	TsigSecret   map[string]string // secret(s) for Tsig map[<zonename>]<base64 secret>, zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2)
+	TsigProvider TsigProvider      // An implementation of the TsigProvider interface. If defined it replaces TsigSecret and is used for all TSIG operations.
+
+	// SingleInflight previously serialised multiple concurrent queries for the
+	// same Qname, Qtype and Qclass to ensure only one would be in flight at a
+	// time.
+	//
+	// Deprecated: This is a no-op. Callers should implement their own in flight
+	// query caching if needed. See github.com/miekg/dns/issues/1449.
+	SingleInflight bool
 }
 
 // Exchange performs a synchronous UDP query. It sends the message m to the address
@@ -185,26 +190,7 @@ func (c *Client) ExchangeWithConn(m *Msg, conn *Conn) (r *Msg, rtt time.Duration
 	return c.exchangeWithConnContext(context.Background(), m, conn)
 }
 
-func (c *Client) exchangeWithConnContext(ctx context.Context, m *Msg, conn *Conn) (r *Msg, rtt time.Duration, err error) {
-	if !c.SingleInflight {
-		return c.exchangeContext(ctx, m, conn)
-	}
-
-	q := m.Question[0]
-	key := fmt.Sprintf("%s:%d:%d", q.Name, q.Qtype, q.Qclass)
-	r, rtt, err, shared := c.group.Do(key, func() (*Msg, time.Duration, error) {
-		// When we're doing singleflight we don't want one context cancellation, cancel _all_ outstanding queries.
-		// Hence we ignore the context and use Background().
-		return c.exchangeContext(context.Background(), m, conn)
-	})
-	if r != nil && shared {
-		r = r.Copy()
-	}
-
-	return r, rtt, err
-}
-
-func (c *Client) exchangeContext(ctx context.Context, m *Msg, co *Conn) (r *Msg, rtt time.Duration, err error) {
+func (c *Client) exchangeWithConnContext(ctx context.Context, m *Msg, co *Conn) (r *Msg, rtt time.Duration, err error) {
 	opt := m.IsEdns0()
 	// If EDNS0 is used use that for size.
 	if opt != nil && opt.UDPSize() >= MinMsgSize {
diff --git a/defaults.go b/defaults.go
index 75b17f0c..c1558b79 100644
--- a/defaults.go
+++ b/defaults.go
@@ -272,18 +272,24 @@ func IsMsg(buf []byte) error {
 
 // IsFqdn checks if a domain name is fully qualified.
 func IsFqdn(s string) bool {
-	s2 := strings.TrimSuffix(s, ".")
-	if s == s2 {
+	// Check for (and remove) a trailing dot, returning if there isn't one.
+	if s == "" || s[len(s)-1] != '.' {
 		return false
 	}
+	s = s[:len(s)-1]
 
-	i := strings.LastIndexFunc(s2, func(r rune) bool {
+	// If we don't have an escape sequence before the final dot, we know it's
+	// fully qualified and can return here.
+	if s == "" || s[len(s)-1] != '\\' {
+		return true
+	}
+
+	// Otherwise we have to check if the dot is escaped or not by checking if
+	// there are an odd or even number of escape sequences before the dot.
+	i := strings.LastIndexFunc(s, func(r rune) bool {
 		return r != '\\'
 	})
-
-	// Test whether we have an even number of escape sequences before
-	// the dot or none.
-	return (len(s2)-i)%2 != 0
+	return (len(s)-i)%2 != 0
 }
 
 // IsRRset checks if a set of RRs is a valid RRset as defined by RFC 2181.
diff --git a/scan.go b/scan.go
index 57be9882..3083c3e5 100644
--- a/scan.go
+++ b/scan.go
@@ -10,13 +10,13 @@ import (
 	"strings"
 )
 
-const maxTok = 2048 // Largest token we can return.
+const maxTok = 512 // Token buffer start size, and growth size amount.
 
 // The maximum depth of $INCLUDE directives supported by the
 // ZoneParser API.
 const maxIncludeDepth = 7
 
-// Tokinize a RFC 1035 zone file. The tokenizer will normalize it:
+// Tokenize a RFC 1035 zone file. The tokenizer will normalize it:
 // * Add ownernames if they are left blank;
 // * Suppress sequences of spaces;
 // * Make each RR fit on one line (_NEWLINE is send as last)
@@ -765,8 +765,8 @@ func (zl *zlexer) Next() (lex, bool) {
 	}
 
 	var (
-		str [maxTok]byte // Hold string text
-		com [maxTok]byte // Hold comment text
+		str = make([]byte, maxTok) // Hold string text
+		com = make([]byte, maxTok) // Hold comment text
 
 		stri int // Offset in str (0 means empty)
 		comi int // Offset in com (0 means empty)
@@ -785,14 +785,12 @@ func (zl *zlexer) Next() (lex, bool) {
 		l.line, l.column = zl.line, zl.column
 
 		if stri >= len(str) {
-			l.token = "token length insufficient for parsing"
-			l.err = true
-			return *l, true
+			// if buffer length is insufficient, increase it.
+			str = append(str[:], make([]byte, maxTok)...)
 		}
 		if comi >= len(com) {
-			l.token = "comment length insufficient for parsing"
-			l.err = true
-			return *l, true
+			// if buffer length is insufficient, increase it.
+			com = append(com[:], make([]byte, maxTok)...)
 		}
 
 		switch x {
@@ -816,7 +814,7 @@ func (zl *zlexer) Next() (lex, bool) {
 			if stri == 0 {
 				// Space directly in the beginning, handled in the grammar
 			} else if zl.owner {
-				// If we have a string and its the first, make it an owner
+				// If we have a string and it's the first, make it an owner
 				l.value = zOwner
 				l.token = string(str[:stri])
 
diff --git a/singleinflight.go b/singleinflight.go
deleted file mode 100644
index febcc300..00000000
--- a/singleinflight.go
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright 2013 The Go Authors.  All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// Adapted for dns package usage by Miek Gieben.
-
-package dns
-
-import "sync"
-import "time"
-
-// call is an in-flight or completed singleflight.Do call
-type call struct {
-	wg   sync.WaitGroup
-	val  *Msg
-	rtt  time.Duration
-	err  error
-	dups int
-}
-
-// singleflight represents a class of work and forms a namespace in
-// which units of work can be executed with duplicate suppression.
-type singleflight struct {
-	sync.Mutex                  // protects m
-	m          map[string]*call // lazily initialized
-
-	dontDeleteForTesting bool // this is only to be used by TestConcurrentExchanges
-}
-
-// Do executes and returns the results of the given function, making
-// sure that only one execution is in-flight for a given key at a
-// time. If a duplicate comes in, the duplicate caller waits for the
-// original to complete and receives the same results.
-// The return value shared indicates whether v was given to multiple callers.
-func (g *singleflight) Do(key string, fn func() (*Msg, time.Duration, error)) (v *Msg, rtt time.Duration, err error, shared bool) {
-	g.Lock()
-	if g.m == nil {
-		g.m = make(map[string]*call)
-	}
-	if c, ok := g.m[key]; ok {
-		c.dups++
-		g.Unlock()
-		c.wg.Wait()
-		return c.val, c.rtt, c.err, true
-	}
-	c := new(call)
-	c.wg.Add(1)
-	g.m[key] = c
-	g.Unlock()
-
-	c.val, c.rtt, c.err = fn()
-	c.wg.Done()
-
-	if !g.dontDeleteForTesting {
-		g.Lock()
-		delete(g.m, key)
-		g.Unlock()
-	}
-
-	return c.val, c.rtt, c.err, c.dups > 0
-}


Test patch:
diff --git a/client_test.go b/client_test.go
index ff0c2187..af1bd23e 100644
--- a/client_test.go
+++ b/client_test.go
@@ -679,58 +679,6 @@ func TestTimeout(t *testing.T) {
 	})
 }
 
-// Check that responses from deduplicated requests aren't shared between callers
-func TestConcurrentExchanges(t *testing.T) {
-	cases := make([]*Msg, 2)
-	cases[0] = new(Msg)
-	cases[1] = new(Msg)
-	cases[1].Truncated = true
-
-	for _, m := range cases {
-		mm := m // redeclare m so as not to trip the race detector
-		handler := func(w ResponseWriter, req *Msg) {
-			r := mm.Copy()
-			r.SetReply(req)
-
-			w.WriteMsg(r)
-		}
-
-		HandleFunc("miek.nl.", handler)
-		defer HandleRemove("miek.nl.")
-
-		s, addrstr, _, err := RunLocalUDPServer(":0")
-		if err != nil {
-			t.Fatalf("unable to run test server: %s", err)
-		}
-		defer s.Shutdown()
-
-		m := new(Msg)
-		m.SetQuestion("miek.nl.", TypeSRV)
-
-		c := &Client{
-			SingleInflight: true,
-		}
-		// Force this client to always return the same request,
-		// even though we're querying sequentially. Running the
-		// Exchange calls below concurrently can fail due to
-		// goroutine scheduling, but this simulates the same
-		// outcome.
-		c.group.dontDeleteForTesting = true
-
-		r := make([]*Msg, 2)
-		for i := range r {
-			r[i], _, _ = c.Exchange(m.Copy(), addrstr)
-			if r[i] == nil {
-				t.Errorf("response %d is nil", i)
-			}
-		}
-
-		if r[0] == r[1] {
-			t.Errorf("got same response, expected non-shared responses")
-		}
-	}
-}
-
 func TestExchangeWithConn(t *testing.T) {
 	HandleFunc("miek.nl.", HelloServer)
 	defer HandleRemove("miek.nl.")
diff --git a/dns_test.go b/dns_test.go
index f1dbf23d..cba5386a 100644
--- a/dns_test.go
+++ b/dns_test.go
@@ -314,3 +314,49 @@ func TestTKEY(t *testing.T) {
 		t.Fatalf("unable to parse TKEY string: %s", newError)
 	}
 }
+
+var (
+	sinkBool   bool
+	sinkString string
+)
+
+func BenchmarkIsFQDN(b *testing.B) {
+	b.Run("no_dot", func(b *testing.B) {
+		var r bool
+		for n := 0; n < b.N; n++ {
+			r = IsFqdn("www.google.com")
+		}
+		sinkBool = r
+	})
+	b.Run("unescaped", func(b *testing.B) {
+		var r bool
+		for n := 0; n < b.N; n++ {
+			r = IsFqdn("www.google.com.")
+		}
+		sinkBool = r
+	})
+	b.Run("escaped", func(b *testing.B) {
+		var r bool
+		for n := 0; n < b.N; n++ {
+			r = IsFqdn(`www.google.com\\\\\\\\.`)
+		}
+		sinkBool = r
+	})
+}
+
+func BenchmarkFQDN(b *testing.B) {
+	b.Run("is_fqdn", func(b *testing.B) {
+		var r string
+		for n := 0; n < b.N; n++ {
+			r = Fqdn("www.google.com.")
+		}
+		sinkString = r
+	})
+	b.Run("not_fqdn", func(b *testing.B) {
+		var r string
+		for n := 0; n < b.N; n++ {
+			r = Fqdn("www.google.com")
+		}
+		sinkString = r
+	})
+}
diff --git a/parse_test.go b/parse_test.go
index 3df65604..6ea85495 100644
--- a/parse_test.go
+++ b/parse_test.go
@@ -1365,8 +1365,8 @@ func TestPrintfVerbsRdata(t *testing.T) {
 
 func TestParseTokenOverflow(t *testing.T) {
 	_, err := NewRR("_443._tcp.example.org. IN TLSA 0 0 0 308205e8308204d0a00302010202100411de8f53b462f6a5a861b712ec6b59300d06092a864886f70d01010b05003070310b300906035504061302555331153013060355040a130c446967694365727420496e6331193017060355040b13107777772e64696769636572742e636f6d312f302d06035504031326446967694365727420534841322048696768204173737572616e636520536572766572204341301e170d3134313130363030303030305a170d3135313131333132303030305a3081a5310b3009060355040613025553311330110603550408130a43616c69666f726e6961311430120603550407130b4c6f7320416e67656c6573313c303a060355040a1333496e7465726e657420436f72706f726174696f6e20666f722041737369676e6564204e616d657320616e64204e756d6265727331133011060355040b130a546563686e6f6c6f6779311830160603550403130f7777772e6578616d706c652e6f726730820122300d06092a864886f70d01010105000382010f003082010a02820101009e663f52a3d18cb67cdfed547408a4e47e4036538988da2798da3b6655f7240d693ed1cb3fe6d6ad3a9e657ff6efa86b83b0cad24e5d31ff2bf70ec3b78b213f1b4bf61bdc669cbbc07d67154128ca92a9b3cbb4213a836fb823ddd4d7cc04918314d25f06086fa9970ba17e357cca9b458c27eb71760ab95e3f9bc898ae89050ae4d09ba2f7e4259d9ff1e072a6971b18355a8b9e53670c3d5dbdbd283f93a764e71b3a4140ca0746090c08510e2e21078d7d07844bf9c03865b531a0bf2ee766bc401f6451c5a1e6f6fb5d5c1d6a97a0abe91ae8b02e89241e07353909ccd5b41c46de207c06801e08f20713603827f2ae3e68cf15ef881d7e0608f70742e30203010001a382024630820242301f0603551d230418301680145168ff90af0207753cccd9656462a212b859723b301d0603551d0e04160414b000a7f422e9b1ce216117c4c46e7164c8e60c553081810603551d11047a3078820f7777772e6578616d706c652e6f7267820b6578616d706c652e636f6d820b6578616d706c652e656475820b6578616d706c652e6e6574820b6578616d706c652e6f7267820f7777772e6578616d706c652e636f6d820f7777772e6578616d706c652e656475820f7777772e6578616d706c652e6e6574300e0603551d0f0101ff0404030205a0301d0603551d250416301406082b0601050507030106082b0601050507030230750603551d1f046e306c3034a032a030862e687474703a2f2f63726c332e64696769636572742e636f6d2f736861322d68612d7365727665722d67332e63726c3034a032a030862e687474703a2f2f63726c342e64696769636572742e636f6d2f736861322d68612d7365727665722d67332e63726c30420603551d20043b3039303706096086480186fd6c0101302a302806082b06010505070201161c68747470733a2f2f7777772e64696769636572742e636f6d2f43505330818306082b0601050507010104773075302406082b060105050730018618687474703a2f2f6f6373702e64696769636572742e636f6d304d06082b060105050730028641687474703a2f2f636163657274732e64696769636572742e636f6d2f446967694365727453484132486967684173737572616e636553657276657243412e637274300c0603551d130101ff04023000300d06092a864886f70d01010b050003820101005eac2124dedb3978a86ff3608406acb542d3cb54cb83facd63aec88144d6a1bf15dbf1f215c4a73e241e582365cba9ea50dd306541653b3513af1a0756c1b2720e8d112b34fb67181efad9c4609bdc670fb025fa6e6d42188161b026cf3089a08369c2f3609fc84bcc3479140c1922ede430ca8dbac2b2a3cdacb305ba15dc7361c4c3a5e6daa99cb446cb221b28078a7a944efba70d96f31ac143d959bccd2fd50e30c325ea2624fb6b6dbe9344dbcf133bfbd5b4e892d635dbf31596451672c6b65ba5ac9b3cddea92b35dab1065cae3c8cb6bb450a62ea2f72ea7c6bdc7b65fa09b012392543734083c7687d243f8d0375304d99ccd2e148966a8637a6797")
-	if err == nil {
-		t.Fatalf("token overflow should return an error")
+	if err != nil {
+		t.Fatalf("long token should not return an error")
 	}
 }
 
@@ -1961,3 +1961,17 @@ func TestParseAMTRELAY(t *testing.T) {
 		}
 	}
 }
+
+func TestParseOPENPGPKEY(t *testing.T) {
+	dt := map[string]string{
+		"2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db._openpgpkey 3600 IN OPENPGPKEY mDMEZCMu8xYJKwYBBAHaRw8BAQdAH4FTbN/H5SoMBl9Ez2cFQ1NuzymK894fq2ffsYDvRkG0EWFsaWNlQGV4YW1wbGUuY29tiJYEExYKAD4CGwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AWIQRjw8oAQytQxDz5Q/Io7xpohfeBngUCZCMv5gUJAAk7ZgAKCRAo7xpohfeBnlmVAP9k0slIpLwddCD1bZ9qVjqzNcS743OIDny7XuH6x02L2wEAwxqAotO7/oUm0L4wyYR6hvGlhuGMSZXc9xMwZ1wVcA8=":     "2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db._openpgpkey.\t3600\tIN\tOPENPGPKEY\tmDMEZCMu8xYJKwYBBAHaRw8BAQdAH4FTbN/H5SoMBl9Ez2cFQ1NuzymK894fq2ffsYDvRkG0EWFsaWNlQGV4YW1wbGUuY29tiJYEExYKAD4CGwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AWIQRjw8oAQytQxDz5Q/Io7xpohfeBngUCZCMv5gUJAAk7ZgAKCRAo7xpohfeBnlmVAP9k0slIpLwddCD1bZ9qVjqzNcS743OIDny7XuH6x02L2wEAwxqAotO7/oUm0L4wyYR6hvGlhuGMSZXc9xMwZ1wVcA8=",
+		"2bb5bc4202aaecd48dcb54967c8e7f1b7574a436f04e0d15534b20e5._openpgpkey 3600 IN OPENPGPKEY mDMEZCMxgRYJKwYBBAHaRw8BAQdA/fgtlQjGflt2MUMWhRZRnH5Hg+BY9sQTeePmqqUs+lK0Fem6u+iho+WtkEBleGFtcGxlLmNvbYiWBBMWCgA+AhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAFiEEIWsEkWx5wygGCb61+tJ3q3m88E0FAmQjMbMFCQAJOqwACgkQ+tJ3q3m88E0z4gEAtowKJMPefyV5YCW8VubgXK7Fa+hjwXOPSsHnEnJw9pUBAL+VZvNZv/VZvyGGMd31Yivqerzl6q+VIkZ6XffVb2AB": "2bb5bc4202aaecd48dcb54967c8e7f1b7574a436f04e0d15534b20e5._openpgpkey.\t3600\tIN\tOPENPGPKEY\tmDMEZCMxgRYJKwYBBAHaRw8BAQdA/fgtlQjGflt2MUMWhRZRnH5Hg+BY9sQTeePmqqUs+lK0Fem6u+iho+WtkEBleGFtcGxlLmNvbYiWBBMWCgA+AhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAFiEEIWsEkWx5wygGCb61+tJ3q3m88E0FAmQjMbMFCQAJOqwACgkQ+tJ3q3m88E0z4gEAtowKJMPefyV5YCW8VubgXK7Fa+hjwXOPSsHnEnJw9pUBAL+VZvNZv/VZvyGGMd31Yivqerzl6q+VIkZ6XffVb2AB",
+		"2bb5bc4202aaecd48dcb54967c8e7f1b7574a436f04e0d15534b20e5._openpgpkey 3600 IN OPENPGPKEY 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": "2bb5bc4202aaecd48dcb54967c8e7f1b7574a436f04e0d15534b20e5._openpgpkey.\t3600\tIN\tOPENPGPKEY\t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",
+	}
+	for i, o := range dt {
+		rr := testRR(i).(*OPENPGPKEY)
+		if s := rr.String(); s != o {
+			t.Errorf("input %#v does not match expected output %#v", s, o)
+		}
+	}
+}