From nobody Fri Jun 18 05:33:17 2021
Return-Path: <stephane@sources.org>
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 577863A192F
 for <dnsop@ietfa.amsl.com>; Fri, 18 Jun 2021 05:33:15 -0700 (PDT)
X-Virus-Scanned: amavisd-new at amsl.com
X-Spam-Flag: NO
X-Spam-Score: -1.65
X-Spam-Level: 
X-Spam-Status: No, score=-1.65 tagged_above=-999 required=5
 tests=[BAYES_00=-1.9, HEADER_FROM_DIFFERENT_DOMAINS=0.249,
 SPF_HELO_NONE=0.001, SPF_PASS=-0.001, URIBL_BLOCKED=0.001]
 autolearn=no 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 3y_WFhjY6RW8 for <dnsop@ietfa.amsl.com>;
 Fri, 18 Jun 2021 05:33:10 -0700 (PDT)
Received: from ayla.bortzmeyer.org (ayla.bortzmeyer.org [92.243.4.211])
 (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 1CE203A16B0
 for <dnsop@ietf.org>; Fri, 18 Jun 2021 05:33:09 -0700 (PDT)
Received: by ayla.bortzmeyer.org (Postfix, from userid 10)
 id 1404DA0278; Fri, 18 Jun 2021 14:33:07 +0200 (CEST)
Received: by mail.sources.org (Postfix, from userid 1000)
 id EE688190B87; Fri, 18 Jun 2021 14:30:52 +0200 (CEST)
Date: Fri, 18 Jun 2021 14:30:52 +0200
From: Stephane Bortzmeyer <bortzmeyer@nic.fr>
To: Hugo Salgado <hsalgado@nic.cl>
Cc: dnsop@ietf.org
Message-ID: <20210618123052.GA20671@sources.org>
References: <162342216090.6059.5920585323129043595@ietfa.amsl.com>
 <20210613161540.GA14433@sources.org> <20210614140322.GA3276@pepino>
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="KsGdsel6WgEHnImy"
Content-Disposition: inline
In-Reply-To: <20210614140322.GA3276@pepino>
X-Transport: UUCP rules
X-Operating-System: Debian GNU/Linux 10.9
X-Charlie: Je suis Charlie
User-Agent: Mutt/1.10.1 (2018-07-13)
Archived-At: <https://mailarchive.ietf.org/arch/msg/dnsop/t_wIZRstRxRBMAZlz49CvrfXzto>
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: Fri, 18 Jun 2021 12:33:15 -0000


--KsGdsel6WgEHnImy
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline

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

--KsGdsel6WgEHnImy
Content-Type: text/x-python; charset=us-ascii
Content-Disposition: attachment; filename="test-rrserial.py"

#!/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())



--KsGdsel6WgEHnImy
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="test-rrserial.go"

// 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)
	}
}

--KsGdsel6WgEHnImy--

