Repository: miekg/dns

Issue:
Bad target domain parsing.
Here are some bad RRs with invalid (empty) target (please note the space before the `;`)

```zone
bad.example.org.        CNAME ; bad cname
bad.example.org.        HTTPS 10 ; bad https
bad.example.org.        MX 10 ; bad mx
bad.example.org.        SRV 1 0 80 ; bad srv
```

This will be parsed to the following result without error, which treat the empty blank as the `\010.`.

```zone
bad.example.org.        CNAME   \010.
bad.example.org.        HTTPS   10 \010.
bad.example.org.        MX      10 \010.
bad.example.org.        SRV     1 0 80 \010. 
```

However, if we trim the space before the `;`

```zone
bad.example.org.        CNAME; bad cname
bad.example.org.        HTTPS 10; bad https
bad.example.org.        MX 10; bad mx
bad.example.org.        SRV 1 0 80; bad srv
```

The parser will trigger the following errors as exptected

```
dns: not a TTL: "CNAME" at line: 1:23
dns: bad SVCB Target: " " at line: 1:23
dns: bad MX Mx: " " at line: 1:20
dns: bad SRV Target: " " at line: 1:21
```

I propose to trigger error for any case that contains empty target.



Discussion:

Solution patch:
diff --git a/README.md b/README.md
index 8c957f6a..0e42858a 100644
--- a/README.md
+++ b/README.md
@@ -194,6 +194,8 @@ Example programs can be found in the `github.com/miekg/exdns` repository.
 * 9461 - Service Binding Mapping for DNS Servers
 * 9462 - Discovery of Designated Resolvers
 * 9460 - SVCB and HTTPS Records
+* 9606 - DNS Resolver Information
+* Draft - Compact Denial of Existence in DNSSEC
 
 ## Loosely Based Upon
 
diff --git a/edns.go b/edns.go
index 0447fd82..91793b90 100644
--- a/edns.go
+++ b/edns.go
@@ -27,6 +27,7 @@ const (
 	EDNS0LOCALSTART   = 0xFDE9  // Beginning of range reserved for local/experimental use (See RFC 6891)
 	EDNS0LOCALEND     = 0xFFFE  // End of range reserved for local/experimental use (See RFC 6891)
 	_DO               = 1 << 15 // DNSSEC OK
+	_CO               = 1 << 14 // Compact Answers OK
 )
 
 // makeDataOpt is used to unpack the EDNS0 option(s) from a message.
@@ -75,7 +76,11 @@ type OPT struct {
 func (rr *OPT) String() string {
 	s := "\n;; OPT PSEUDOSECTION:\n; EDNS: version " + strconv.Itoa(int(rr.Version())) + "; "
 	if rr.Do() {
-		s += "flags: do; "
+		if rr.Co() {
+			s += "flags: do, co; "
+		} else {
+			s += "flags: do; "
+		}
 	} else {
 		s += "flags:; "
 	}
@@ -195,14 +200,34 @@ func (rr *OPT) SetDo(do ...bool) {
 	}
 }
 
-// Z returns the Z part of the OPT RR as a uint16 with only the 15 least significant bits used.
+// Co returns the value of the CO (Compact Answers OK) bit.
+func (rr *OPT) Co() bool {
+	return rr.Hdr.Ttl&_CO == _CO
+}
+
+// SetCo sets the CO (Compact Answers OK) bit.
+// If we pass an argument, set the CO bit to that value.
+// It is possible to pass 2 or more arguments, but they will be ignored.
+func (rr *OPT) SetCo(co ...bool) {
+	if len(co) == 1 {
+		if co[0] {
+			rr.Hdr.Ttl |= _CO
+		} else {
+			rr.Hdr.Ttl &^= _CO
+		}
+	} else {
+		rr.Hdr.Ttl |= _CO
+	}
+}
+
+// Z returns the Z part of the OPT RR as a uint16 with only the 14 least significant bits used.
 func (rr *OPT) Z() uint16 {
-	return uint16(rr.Hdr.Ttl & 0x7FFF)
+	return uint16(rr.Hdr.Ttl & 0x3FFF)
 }
 
-// SetZ sets the Z part of the OPT RR, note only the 15 least significant bits of z are used.
+// SetZ sets the Z part of the OPT RR, note only the 14 least significant bits of z are used.
 func (rr *OPT) SetZ(z uint16) {
-	rr.Hdr.Ttl = rr.Hdr.Ttl&^0x7FFF | uint32(z&0x7FFF)
+	rr.Hdr.Ttl = rr.Hdr.Ttl&^0x3FFF | uint32(z&0x3FFF)
 }
 
 // EDNS0 defines an EDNS0 Option. An OPT RR can have multiple options appended to it.
diff --git a/scan.go b/scan.go
index e26e8027..57b2adbf 100644
--- a/scan.go
+++ b/scan.go
@@ -108,6 +108,8 @@ type ttlState struct {
 // origin for resolving relative domain names defaults to the DNS root (.).
 // Full zone file syntax is supported, including directives like $TTL and $ORIGIN.
 // All fields of the returned RR are set from the read data, except RR.Header().Rdlength which is set to 0.
+// Is you need a partial resource record with no rdata - for instance - for dynamic updates, see the [ANY]
+// documentation.
 func NewRR(s string) (RR, error) {
 	if len(s) > 0 && s[len(s)-1] != '\n' { // We need a closing newline
 		return ReadRR(strings.NewReader(s+"\n"), "")
@@ -1316,6 +1318,10 @@ func toAbsoluteName(name, origin string) (absolute string, ok bool) {
 		return origin, true
 	}
 
+	if name == "\n" {
+		return "", false
+	}
+
 	// require a valid domain name
 	_, ok = IsDomainName(name)
 	if !ok || name == "" {
diff --git a/scan_rr.go b/scan_rr.go
index c1a76995..ac885f66 100644
--- a/scan_rr.go
+++ b/scan_rr.go
@@ -1620,6 +1620,16 @@ func (rr *NINFO) parse(c *zlexer, o string) *ParseError {
 	return nil
 }
 
+// Uses the same format as TXT
+func (rr *RESINFO) parse(c *zlexer, o string) *ParseError {
+	s, e := endingToTxtSlice(c, "bad RESINFO Resinfo")
+	if e != nil {
+		return e
+	}
+	rr.Txt = s
+	return nil
+}
+
 func (rr *URI) parse(c *zlexer, o string) *ParseError {
 	l, _ := c.Next()
 	i, e := strconv.ParseUint(l.token, 10, 16)
diff --git a/types.go b/types.go
index 7a34c14c..e39cf2fe 100644
--- a/types.go
+++ b/types.go
@@ -101,6 +101,7 @@ const (
 	TypeCAA        uint16 = 257
 	TypeAVC        uint16 = 258
 	TypeAMTRELAY   uint16 = 260
+	TypeRESINFO    uint16 = 261
 
 	TypeTKEY uint16 = 249
 	TypeTSIG uint16 = 250
@@ -267,11 +268,20 @@ func (q *Question) String() (s string) {
 	return s
 }
 
-// ANY is a wild card record. See RFC 1035, Section 3.2.3. ANY
-// is named "*" there.
+// ANY is a wild card record. See RFC 1035, Section 3.2.3. ANY is named "*" there.
+// The ANY records can be (ab)used to create resource records without any rdata, that
+// can be used in dynamic update requests. Basic use pattern:
+//
+//	a := &ANY{RR_Header{
+//		Name:   "example.org.",
+//		Rrtype: TypeA,
+//		Class:  ClassINET,
+//	}}
+//
+// Results in an A record without rdata.
 type ANY struct {
 	Hdr RR_Header
-	// Does not have any rdata
+	// Does not have any rdata.
 }
 
 func (rr *ANY) String() string { return rr.Hdr.String() }
@@ -1508,6 +1518,15 @@ func (rr *ZONEMD) String() string {
 		" " + rr.Digest
 }
 
+// RESINFO RR. See RFC 9606.
+
+type RESINFO struct {
+	Hdr RR_Header
+	Txt []string `dns:"txt"`
+}
+
+func (rr *RESINFO) String() string { return rr.Hdr.String() + sprintTxt(rr.Txt) }
+
 // APL RR. See RFC 3123.
 type APL struct {
 	Hdr      RR_Header
diff --git a/udp.go b/udp.go
index c018ad43..d2267185 100644
--- a/udp.go
+++ b/udp.go
@@ -1,5 +1,5 @@
-//go:build !windows
-// +build !windows
+//go:build !windows && !darwin
+// +build !windows,!darwin
 
 package dns
 
diff --git a/udp_windows.go b/udp_no_control.go
similarity index 85%
rename from udp_windows.go
rename to udp_no_control.go
index a259b67e..ca3d4a63 100644
--- a/udp_windows.go
+++ b/udp_no_control.go
@@ -1,9 +1,11 @@
-//go:build windows
-// +build windows
+//go:build windows || darwin
+// +build windows darwin
 
 // TODO(tmthrgd): Remove this Windows-specific code if go.dev/issue/7175 and
 //   go.dev/issue/7174 are ever fixed.
 
+// NOTICE(stek29): darwin supports PKTINFO in sendmsg, but it unbinds sockets, see https://github.com/miekg/dns/issues/724
+
 package dns
 
 import "net"
diff --git a/update.go b/update.go
index 16f9ee85..2fef1461 100644
--- a/update.go
+++ b/update.go
@@ -2,6 +2,7 @@ package dns
 
 // NameUsed sets the RRs in the prereq section to
 // "Name is in use" RRs. RFC 2136 section 2.4.4.
+// See [ANY] on how to make RRs without rdata.
 func (u *Msg) NameUsed(rr []RR) {
 	if u.Answer == nil {
 		u.Answer = make([]RR, 0, len(rr))
@@ -41,6 +42,7 @@ func (u *Msg) Used(rr []RR) {
 
 // RRsetUsed sets the RRs in the prereq section to
 // "RRset exists (value independent -- no rdata)" RRs. RFC 2136 section 2.4.1.
+// See [ANY] on how to make RRs without rdata.
 func (u *Msg) RRsetUsed(rr []RR) {
 	if u.Answer == nil {
 		u.Answer = make([]RR, 0, len(rr))
@@ -53,6 +55,7 @@ func (u *Msg) RRsetUsed(rr []RR) {
 
 // RRsetNotUsed sets the RRs in the prereq section to
 // "RRset does not exist" RRs. RFC 2136 section 2.4.3.
+// See [ANY] on how to make RRs without rdata.
 func (u *Msg) RRsetNotUsed(rr []RR) {
 	if u.Answer == nil {
 		u.Answer = make([]RR, 0, len(rr))
@@ -64,6 +67,7 @@ func (u *Msg) RRsetNotUsed(rr []RR) {
 }
 
 // Insert creates a dynamic update packet that adds an complete RRset, see RFC 2136 section 2.5.1.
+// See [ANY] on how to make RRs without rdata.
 func (u *Msg) Insert(rr []RR) {
 	if len(u.Question) == 0 {
 		panic("dns: empty question section")
@@ -78,6 +82,7 @@ func (u *Msg) Insert(rr []RR) {
 }
 
 // RemoveRRset creates a dynamic update packet that deletes an RRset, see RFC 2136 section 2.5.2.
+// See [ANY] on how to make RRs without rdata.
 func (u *Msg) RemoveRRset(rr []RR) {
 	if u.Ns == nil {
 		u.Ns = make([]RR, 0, len(rr))
@@ -89,6 +94,7 @@ func (u *Msg) RemoveRRset(rr []RR) {
 }
 
 // RemoveName creates a dynamic update packet that deletes all RRsets of a name, see RFC 2136 section 2.5.3
+// See [ANY] on how to make RRs without rdata.
 func (u *Msg) RemoveName(rr []RR) {
 	if u.Ns == nil {
 		u.Ns = make([]RR, 0, len(rr))
@@ -99,6 +105,7 @@ func (u *Msg) RemoveName(rr []RR) {
 }
 
 // Remove creates a dynamic update packet deletes RR from a RRSset, see RFC 2136 section 2.5.4
+// See [ANY] on how to make RRs without rdata.
 func (u *Msg) Remove(rr []RR) {
 	if u.Ns == nil {
 		u.Ns = make([]RR, 0, len(rr))
diff --git a/version.go b/version.go
index e290e3df..73e34edc 100644
--- a/version.go
+++ b/version.go
@@ -3,7 +3,7 @@ package dns
 import "fmt"
 
 // Version is current version of this library.
-var Version = v{1, 1, 63}
+var Version = v{1, 1, 65}
 
 // v holds the version of this library.
 type v struct {
diff --git a/zduplicate.go b/zduplicate.go
index 330c0539..ebd9e029 100644
--- a/zduplicate.go
+++ b/zduplicate.go
@@ -957,6 +957,23 @@ func (r1 *PX) isDuplicate(_r2 RR) bool {
 	return true
 }
 
+func (r1 *RESINFO) isDuplicate(_r2 RR) bool {
+	r2, ok := _r2.(*RESINFO)
+	if !ok {
+		return false
+	}
+	_ = r2
+	if len(r1.Txt) != len(r2.Txt) {
+		return false
+	}
+	for i := 0; i < len(r1.Txt); i++ {
+		if r1.Txt[i] != r2.Txt[i] {
+			return false
+		}
+	}
+	return true
+}
+
 func (r1 *RFC3597) isDuplicate(_r2 RR) bool {
 	r2, ok := _r2.(*RFC3597)
 	if !ok {
diff --git a/zmsg.go b/zmsg.go
index 5a6cf4c6..cc09810f 100644
--- a/zmsg.go
+++ b/zmsg.go
@@ -762,6 +762,14 @@ func (rr *PX) pack(msg []byte, off int, compression compressionMap, compress boo
 	return off, nil
 }
 
+func (rr *RESINFO) pack(msg []byte, off int, compression compressionMap, compress bool) (off1 int, err error) {
+	off, err = packStringTxt(rr.Txt, msg, off)
+	if err != nil {
+		return off, err
+	}
+	return off, nil
+}
+
 func (rr *RFC3597) pack(msg []byte, off int, compression compressionMap, compress bool) (off1 int, err error) {
 	off, err = packStringHex(rr.Rdata, msg, off)
 	if err != nil {
@@ -2353,6 +2361,17 @@ func (rr *PX) unpack(msg []byte, off int) (off1 int, err error) {
 	return off, nil
 }
 
+func (rr *RESINFO) unpack(msg []byte, off int) (off1 int, err error) {
+	rdStart := off
+	_ = rdStart
+
+	rr.Txt, off, err = unpackStringTxt(msg, off)
+	if err != nil {
+		return off, err
+	}
+	return off, nil
+}
+
 func (rr *RFC3597) unpack(msg []byte, off int) (off1 int, err error) {
 	rdStart := off
 	_ = rdStart
diff --git a/ztypes.go b/ztypes.go
index 11f13ecf..cea79ae7 100644
--- a/ztypes.go
+++ b/ztypes.go
@@ -66,6 +66,7 @@ var TypeToRR = map[uint16]func() RR{
 	TypeOPT:        func() RR { return new(OPT) },
 	TypePTR:        func() RR { return new(PTR) },
 	TypePX:         func() RR { return new(PX) },
+	TypeRESINFO:    func() RR { return new(RESINFO) },
 	TypeRKEY:       func() RR { return new(RKEY) },
 	TypeRP:         func() RR { return new(RP) },
 	TypeRRSIG:      func() RR { return new(RRSIG) },
@@ -154,6 +155,7 @@ var TypeToString = map[uint16]string{
 	TypeOPT:        "OPT",
 	TypePTR:        "PTR",
 	TypePX:         "PX",
+	TypeRESINFO:    "RESINFO",
 	TypeRKEY:       "RKEY",
 	TypeRP:         "RP",
 	TypeRRSIG:      "RRSIG",
@@ -238,6 +240,7 @@ func (rr *OPENPGPKEY) Header() *RR_Header { return &rr.Hdr }
 func (rr *OPT) Header() *RR_Header        { return &rr.Hdr }
 func (rr *PTR) Header() *RR_Header        { return &rr.Hdr }
 func (rr *PX) Header() *RR_Header         { return &rr.Hdr }
+func (rr *RESINFO) Header() *RR_Header    { return &rr.Hdr }
 func (rr *RFC3597) Header() *RR_Header    { return &rr.Hdr }
 func (rr *RKEY) Header() *RR_Header       { return &rr.Hdr }
 func (rr *RP) Header() *RR_Header         { return &rr.Hdr }
@@ -622,6 +625,14 @@ func (rr *PX) len(off int, compression map[string]struct{}) int {
 	return l
 }
 
+func (rr *RESINFO) len(off int, compression map[string]struct{}) int {
+	l := rr.Hdr.len(off, compression)
+	for _, x := range rr.Txt {
+		l += len(x) + 1
+	}
+	return l
+}
+
 func (rr *RFC3597) len(off int, compression map[string]struct{}) int {
 	l := rr.Hdr.len(off, compression)
 	l += len(rr.Rdata) / 2
@@ -1148,6 +1159,10 @@ func (rr *PX) copy() RR {
 	}
 }
 
+func (rr *RESINFO) copy() RR {
+	return &RESINFO{rr.Hdr, cloneSlice(rr.Txt)}
+}
+
 func (rr *RFC3597) copy() RR {
 	return &RFC3597{rr.Hdr, rr.Rdata}
 }



Test patch:
diff --git a/edns_test.go b/edns_test.go
index b7c15f7e..b9853dd2 100644
--- a/edns_test.go
+++ b/edns_test.go
@@ -56,6 +56,39 @@ func TestOPTTtl(t *testing.T) {
 		t.Errorf("DO bit should be non-zero")
 	}
 
+	// CO (Compact ANswers OK) flag tests follow the same pattern as DO tests
+	// verify that invoking SetCo() sets CO=1
+	e.SetCo()
+	if !e.Co() {
+		t.Errorf("CO bit should be non-zero")
+	}
+
+	// verify that using SetCo(true) works when CO=1
+	e.SetCo(true)
+	if !e.Co() {
+		t.Errorf("CO bit should still be non-zero")
+	}
+	// verify that we can use SetCo(false) to set CO=0
+	e.SetCo(false)
+	if e.Co() {
+		t.Errorf("CO bit should be zero")
+	}
+	// verify that if we call SetCo(false) when CO=0 that it is unchanged
+	e.SetCo(false)
+	if e.Co() {
+		t.Errorf("CO bit should still be zero")
+	}
+	// verify that using SetCo(true) works for CO=0
+	e.SetCo(true)
+	if !e.Co() {
+		t.Errorf("CO bit should be non-zero")
+	}
+	// verify that using SetCo() works for CO=1
+	e.SetCo()
+	if !e.Co() {
+		t.Errorf("CO bit should be non-zero")
+	}
+
 	if e.Version() != 0 {
 		t.Errorf("version should be non-zero")
 	}
@@ -141,6 +174,7 @@ func TestZ(t *testing.T) {
 	e.Hdr.Rrtype = TypeOPT
 	e.SetVersion(8)
 	e.SetDo()
+	e.SetCo()
 	if e.Z() != 0 {
 		t.Errorf("expected Z of 0, got %d", e.Z())
 	}
@@ -149,8 +183,8 @@ func TestZ(t *testing.T) {
 		t.Errorf("expected Z of 5, got %d", e.Z())
 	}
 	e.SetZ(0xFFFF)
-	if e.Z() != 0x7FFF {
-		t.Errorf("expected Z of 0x7FFFF, got %d", e.Z())
+	if e.Z() != 0x3FFF {
+		t.Errorf("expected Z of 0x3FFFF, got %d", e.Z())
 	}
 	if e.Version() != 8 {
 		t.Errorf("expected version to still be 8, got %d", e.Version())
diff --git a/scan_test.go b/scan_test.go
index c4f7e7f4..e46b3684 100644
--- a/scan_test.go
+++ b/scan_test.go
@@ -226,6 +226,22 @@ func TestZoneParserAddressAAAA(t *testing.T) {
 	}
 }
 
+func TestZoneParserTargetBad(t *testing.T) {
+	records := []string{
+		"bad.example.org. CNAME ; bad cname",
+		"bad.example.org. HTTPS 10 ; bad https",
+		"bad.example.org. MX 10 ; bad mx",
+		"bad.example.org. SRV 1 0 80 ; bad srv",
+	}
+
+	for _, record := range records {
+		const expect = "bad "
+		if got, err := NewRR(record); err == nil || !strings.Contains(err.Error(), expect) {
+			t.Errorf("NewRR(%v) = %v, want err to contain %q", record, got, expect)
+		}
+	}
+}
+
 func TestZoneParserAddressBad(t *testing.T) {
 	records := []string{
 		"1.bad.example.org. 600 IN A ::1",