Subject: [DNSOP] I-D Action: draft-ietf-dnsop-rrserial-00.txt

From: [email-masked]
Date: Fri, 11 Jun 2021 07:36:00 -0700

A New Internet-Draft is available from the on-line Internet-Drafts directories.
This draft is a work item of the Domain Name System Operations WG of the IETF.

        Title           : The "RRSERIAL" EDNS option for the SOA serial of a RR's zone
        Authors         : Hugo Salgado
                          Mauricio Vergara Ereche
	Filename        : draft-ietf-dnsop-rrserial-00.txt
	Pages           : 6
	Date            : 2021-06-11

Abstract:
   The "RRSERIAL" EDNS option allows a DNS querier to request a DNS
   authoritative server to add an EDNS option in the answer of such
   query with the SOA serial number field of the origin zone which
   contains the answered Resource Record.

   This "RRSERIAL" data allows to debug and diagnose problems by helping
   to recognize the data source of an answer in an atomic single query,
   by associating the response with a respective zone version.

The IETF datatracker status page for this draft is:
https://datatracker.ietf.org/doc/draft-ietf-dnsop-rrserial/

There is also an HTML version available at:
https://www.ietf.org/archive/id/draft-ietf-dnsop-rrserial-00.html

Internet-Drafts are also available by anonymous FTP at:
ftp://ftp.ietf.org/internet-drafts/

---

From: Stephane Bortzmeyer <[email-masked]>
Date: Sun, 13 Jun 2021 18:15:40 +0200

On Fri, Jun 11, 2021 at 07:36:00AM -0700,
 [email-masked] <[email-masked]> wrote 

> 	Filename        : draft-ietf-dnsop-rrserial-00.txt

I do not understand why the RRSERIAL EDNS is not added for NXDOMAIN
responses (section 3.2). Specially since it is added for SERVFAIL,
which is surprising.

---

From: Hugo Salgado <[email-masked]>
Date: Mon, 14 Jun 2021 10:03:22 -0400

> responses (section 3.2). Specially since it is added for SERVFAIL,

Originally it was specified only for NOERROR, but after a suggestion in
the list it was seen that it could have value for some cases of SERVFAIL
where there are problems with certain RRs but it is possible to return
the RRSERIAL of the queried zone, which can help debugging.

In the case of NXDOMAIN, the reason for not adding RRSERIAL is because
the response already has the SOA in the AUTHORITY, which would make it
redundant.

Hugo

---

From: Stephane Bortzmeyer <[email-masked]>
Date: Fri, 18 Jun 2021 14:30:52 +0200

On Mon, Jun 14, 2021 at 10:03:22AM -0400,
 Hugo Salgado <[email-masked]> wrote 
 a message of 55 lines which said:

> In the case of NXDOMAIN, the reason for not adding RRSERIAL is
> because the response already has the SOA in the AUTHORITY, which
> would make it redundant.

OK, I see. Here are two implementations of the client part, in Python
and in Go, using this algorithm (RRSERIAL if NOERROR or SERVFAIL, and
the SOA record if NXDOMAIN).

Python :

% ./test-rrserial.py 200.1.122.30 dateserial.example.com TXT
dateserial.example.com. 43200 IN TXT "Test zone for RRSERIAL record"      
Serial of the answer is 2018081401

% ./test-rrserial.py 200.1.122.30 dateserial.example.com LOC
No value for dateserial.example.com/LOC
Serial found from SOA record: 2018081401

Go :

%   ./test-rrserial 200.1.122.30 incserial.example.com  MX
incserial.example.com.	43200	IN	MX	10 mail.incserial.example.com.
EDNS rrserial found, "1"

%   ./test-rrserial 200.1.122.30 incserial.example.com  SSHFP
Empty answer received
EDNS rrserial found, "1"

Note that you can do it with dig alone but the display of unknown data
is less pretty:

% dig +ednsopt=65024 @200.1.122.30 dateserial.example.com
...
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 4096
; OPT=65024: 78 49 7a 79 ("xIzy")

Note also there is a bug in your server: when the name does not exist,
it sends incorrect messages. dig says "Message parser reports
malformed message packet" and the Python library "DNS message is
malformed". And the Go library:

%   ./test-rrserial 200.1.122.30 doesnotexist.incserial.example.com
Error in query: dns: overflowing header size

---

From: Hugo Salgado <[email-masked]>
Date: Fri, 18 Jun 2021 21:22:56 -0400

Hi Stephane. Thanks a lot for your implementations!

I have a modified dig version with support for rrserial in:
  https://gitlab.isc.org/huguei/bind9/-/tree/rrserial

; <<>> DiG 9.17.14 <<>> @200.1.122.30 dateserial.example.com txt +rrserial +nsid
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 34574
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 2
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
; NSID: 63 6c 6e 73 64 74 65 73 74 ("clnsdtest")
; RRSERIAL: 78 49 7a 79 ("2018081401")
;; QUESTION SECTION:
;dateserial.example.com.		IN	TXT

;; ANSWER SECTION:
[ ... ]

Also, I can reproduce the bug with NXDOMAINs with the NSD
implementation :( Thanks for the notice, I'll look into it ASAP!

I plan to keep track to these prototypes and scripts in some wiki too.

Best,

Hugo

> #!/usr/bin/env python3
> 
> """Simple Python program to implement IETF draft
> draft-ietf-dnsop-rrserial (hereafter "the draft")
> 
> If you don't know Python:
> pip install dnspython
> ./test-rrserial.py 200.1.122.30 dateserial.example.com
> 
> """
> 
> import struct
> import sys
> 
> # DNSpython https://www.dnspython.org/ We probably require 2.0 or higher.
> import dns.message
> import dns.query
> import dns.edns
> import dns.rdatatype
> 
> # Temporary value in the draft
> dns.edns.RRSERIAL = 65024
> 
> def print_soa(response):
>   found = False
>   for rr in response.authority:
>     if rr.rdtype == dns.rdatatype.SOA:
>       found = True
>       print("Serial found from SOA record: %s" % rr[0].serial)
>   if not found:
>     print("Negative answer without a SOA record :-(")
> 
> def print_rrserial(response):
>   found = False
>   for opt in response.options:
>     if opt.otype == dns.edns.RRSERIAL:
>       found = True
>       print("Serial of the answer is %s" % struct.unpack(">I", opt.data)[0])
>   if not found:
>     print("No EDNS serial option in answer")
> 
> # TODO: it would probably be better (more pythonic) to inherit from
> # dns.edns.GenericOption and provide a parser.
> opts = [dns.edns.GenericOption(dns.edns.RRSERIAL, b'')]
> 
> if len(sys.argv) != 3 and len(sys.argv) != 4:
>   raise Exception("Usage: %s server qname qtype [for instance '200.1.122.30 dateserial.example.com AAAA')" % sys.argv[0])
> server = sys.argv[1]
> qname = sys.argv[2]
> if len(sys.argv) == 4:
>   qtype = dns.rdatatype.from_text(sys.argv[3])
> else:
>   qtype = dns.rdatatype.AAAA
> message = dns.message.make_query(qname, qtype, options=opts)
> response = dns.query.udp(message, server)
> if response.rcode() == dns.rcode.NOERROR:
>   data = False
>   for rr in response.answer:
>     if rr.rdtype == qtype:
>       data = True
>       print(rr)
>   if not data:
>     print("No value for %s/%s" % (qname, dns.rdatatype.to_text(qtype)))
>     print_soa(response)
>   else:
>     print_rrserial(response)
> elif response.rcode() == dns.rcode.NXDOMAIN:
>   print("%s not found" % qname)
>   print_soa(response)
> elif response.rcode() == dns.rcode.SERVFAIL:
>   print("%s failed" % qname) 
>   print_rrserial(response)
> else:
>   print("Unknown return code %s" % response.rcode())
> 
> 

> // Test program to experiment the IETF draft draft-ietf-dnsop-rrserial (hereafter "the draft").
> // Depends on godns <https://miek.nl/2014/august/16/go-dns-package/> <https://github.com/miekg/dns>.
> 
> // If you don't know Go:
> //   go get github.com/miekg/dns
> //   go build test-rrserial.go
> 
> package main
> 
> import (
> 	"encoding/binary"
> 	"fmt"
> 	"github.com/miekg/dns"
> 	"os"
> 	"strings"
> )
> 
> const otype = 65024
> 
> func print_rrserial(msg *dns.Msg) {
> 	opt := msg.IsEdns0()
> 	for _, v := range opt.Option {
> 		switch v := v.(type) {
> 		case *dns.EDNS0_LOCAL:
> 			if v.Option() == otype {
> 				serial := binary.BigEndian.Uint32(v.Data)
> 				fmt.Printf("EDNS rrserial found, \"%d\"\n", serial)
> 			}
> 		default:
> 			continue
> 		}
> 	}
> }
> 
> func main() {
> 	if len(os.Args) != 3 && len(os.Args) != 4 {
> 		fmt.Printf("%s SERVER QNAME [QTYPE]\n", os.Args[0])
> 		os.Exit(1)
> 	}
> 	ns := os.Args[1]
> 	qname := dns.Fqdn(os.Args[2])
> 	qtype := dns.TypeAAAA
> 	ok := true
> 	if len(os.Args) == 4 {
> 		if qtype, ok = dns.StringToType[strings.ToUpper(os.Args[3])]; ok {
> 			// Good value
> 		} else {
> 			fmt.Printf("%s is not a known record type\n", strings.ToUpper(os.Args[3]))
> 			os.Exit(1)
> 		}
> 	}
> 	m := new(dns.Msg)
> 	m.Question = make([]dns.Question, 1)
> 	c := new(dns.Client)
> 	o := new(dns.OPT)
> 	o.Hdr.Name = "."
> 	o.Hdr.Rrtype = dns.TypeOPT
> 	o.SetUDPSize(4096)
> 	e := new(dns.EDNS0_LOCAL)
> 	e.Code = otype
> 	e.Data = []byte{}
> 	o.Option = append(o.Option, e)
> 	m.Extra = append(m.Extra, o)
> 	m.Question[0] = dns.Question{qname, qtype, dns.ClassINET}
> 	in, _, err := c.Exchange(m, ns+":53")
> 	if err == nil && in != nil {
> 		if in.Rcode == dns.RcodeRefused {
> 			fmt.Printf("Query refused (may be %s is a resolver, we do not ask for recursion)\n", ns)
> 			os.Exit(1)
> 		} else if in.Rcode == dns.RcodeFormatError {
> 			fmt.Printf("%s claims our format is incorrect, it is wrong\n", ns)
> 			os.Exit(1)
> 		} else if in.Rcode == dns.RcodeNameError {
> 			fmt.Printf("%s not found\n", qname)
> 			gotSoa := false
> 			serial := uint32(0)
> 			for _, rsoa := range in.Ns {
> 				switch rsoa.(type) {
> 				case *dns.SOA:
> 					serial = rsoa.(*dns.SOA).Serial
> 					gotSoa = true
> 				default:
> 					continue
> 				}
> 			}
> 			if !gotSoa {
> 				fmt.Printf("No SOA in the answer :-(\n")
> 				os.Exit(1)
> 			} else {
> 				fmt.Printf("Serial number in SOA %d\n", serial)
> 			}
> 			os.Exit(0)
> 		} else if in.Rcode == dns.RcodeServerFailure {
> 			fmt.Printf("Server failure\n")
> 			print_rrserial(in)
> 			os.Exit(1)
> 		} else if in.Rcode != dns.RcodeSuccess {
> 			fmt.Printf("Wrong return code %d\n", in.Rcode)
> 			os.Exit(1)
> 		}
> 		if len(in.Answer) > 0 {
> 			for _, rec := range in.Answer {
> 				fmt.Printf("%s\n", rec)
> 			}
> 		} else if len(in.Answer) == 0 {
> 			fmt.Printf("Empty answer received\n")
> 		}
> 		print_rrserial(in)
> 	} else {
> 		if err != nil {
> 			fmt.Printf("Error in query: %s\n", err)
> 		} else if in == nil {
> 			fmt.Printf("No answer received\n")
> 		}
> 		os.Exit(1)
> 	}
> }