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

Hugo Salgado <hsalgado@nic.cl> Sat, 19 June 2021 01:23 UTC

Return-Path: <hsalgado@nic.cl>
X-Original-To: dnsop@ietfa.amsl.com
Delivered-To: dnsop@ietfa.amsl.com
Received: from localhost (localhost [127.0.0.1]) by ietfa.amsl.com (Postfix) with ESMTP id 2847E3A1A50 for <dnsop@ietfa.amsl.com>; Fri, 18 Jun 2021 18:23:06 -0700 (PDT)
X-Virus-Scanned: amavisd-new at amsl.com
X-Spam-Flag: NO
X-Spam-Score: -1.9
X-Spam-Level:
X-Spam-Status: No, score=-1.9 tagged_above=-999 required=5 tests=[BAYES_00=-1.9, SPF_PASS=-0.001, URIBL_BLOCKED=0.001] autolearn=ham autolearn_force=no
Received: from mail.ietf.org ([4.31.198.44]) by localhost (ietfa.amsl.com [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id D1ej6QIh00Bq for <dnsop@ietfa.amsl.com>; Fri, 18 Jun 2021 18:23:01 -0700 (PDT)
Received: from mail.nic.cl (mail.nic.cl [200.1.123.8]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by ietfa.amsl.com (Postfix) with ESMTPS id 9D7873A1A4E for <dnsop@ietf.org>; Fri, 18 Jun 2021 18:23:00 -0700 (PDT)
Received: from mail.nic.cl (localhost [127.0.0.1]) by mail.nic.cl (Postfix) with ESMTP id 27BBF195D5BB2; Fri, 18 Jun 2021 21:22:58 -0400 (-04)
Received: from pepino (unknown [190.163.103.228]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mail.nic.cl (Postfix) with ESMTPSA id 07673195D5B9F; Fri, 18 Jun 2021 21:22:57 -0400 (-04)
Date: Fri, 18 Jun 2021 21:22:56 -0400
From: Hugo Salgado <hsalgado@nic.cl>
To: Stephane Bortzmeyer <bortzmeyer@nic.fr>
Cc: dnsop@ietf.org
Message-ID: <20210619012256.GA68270@pepino>
References: <162342216090.6059.5920585323129043595@ietfa.amsl.com> <20210613161540.GA14433@sources.org> <20210614140322.GA3276@pepino> <20210618123052.GA20671@sources.org>
MIME-Version: 1.0
Content-Type: multipart/signed; micalg="pgp-sha512"; protocol="application/pgp-signature"; boundary="NzB8fVQJ5HfG6fxh"
Content-Disposition: inline
In-Reply-To: <20210618123052.GA20671@sources.org>
X-Virus-Scanned: ClamAV using ClamSMTP on Fri Jun 18 21:22:58 2021 -0400 (-04) (mail.nic.cl)
Archived-At: <https://mailarchive.ietf.org/arch/msg/dnsop/LVZ_Nv6yIiUPN2G5ZvuSbtnpRgY>
Subject: Re: [DNSOP] I-D Action: draft-ietf-dnsop-rrserial-00.txt
X-BeenThere: dnsop@ietf.org
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: IETF DNSOP WG mailing list <dnsop.ietf.org>
List-Unsubscribe: <https://www.ietf.org/mailman/options/dnsop>, <mailto:dnsop-request@ietf.org?subject=unsubscribe>
List-Archive: <https://mailarchive.ietf.org/arch/browse/dnsop/>
List-Post: <mailto:dnsop@ietf.org>
List-Help: <mailto:dnsop-request@ietf.org?subject=help>
List-Subscribe: <https://www.ietf.org/mailman/listinfo/dnsop>, <mailto:dnsop-request@ietf.org?subject=subscribe>
X-List-Received-Date: Sat, 19 Jun 2021 01:23:06 -0000

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:
dateserial.example.com.	43200	IN	TXT	"Test zone for RRSERIAL record"
[ ... ]


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

On 14:30 18/06, Stephane Bortzmeyer wrote:
> On Mon, Jun 14, 2021 at 10:03:22AM -0400,
>  Hugo Salgado <hsalgado@nic.cl> 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

> #!/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)
> 	}
> }