Processing new EDNS options in a program

The DNS protocol makes it possible to attach metadata to questions or answers through the EDNS extension, standardized in RFC 6891. A number of options are already standardized and can be manipulated from a program, via a DNS library. However, what if this is a new EDNS option, not yet processed by the library?

We will see this with the “RRSERIAL” option currently under discussion at the IETF, in draft-ietf-dnsop-rrserial. Its purpose is to retrieve the serial number of the zone corresponding to a response. It is very simple, the associated value being empty in the question, and only a 32-bit integer in the answer. An experimental server exists using this option, 200.1.122.30.

Already, we can test without programming with dig, and its option +ednsopt. EDNS options have a code, registered with IANA. RRSERIAL does not have one yet, so the test server uses the temporary code 65024:

% 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")

It works, we got an answer. The command dig does not know how to format it properly ("xIzy" is the representation of 78 49 7a 79 interpreted as ASCII, whereas it is an integer, the number 2018081401), but it is already that (the draft specifies that the RRSERIAL option has a null value in the request, only its presence counts. If it had been necessary to give a value, dig allows to do it with +ednsopt=CODE:VALUE). So, the server is working good. Now, we would like to do better and therefore use a suitable DNS client (especially since there are cases to be treated such as the NXDOMAIN response, where the serial number of the zone is in an SOA record, not in the EDNS option). So we are going to program.

Let's start in Python with the dnspython library. We can build the option in the request with the GenericOption class:

opts = [dns.edns.GenericOption(dns.edns.RRSERIAL, b'')]
...
message = dns.message.make_query(qname, qtype, options=opts)

The b'' indicates an empty binary value. To read the option in the response:

for opt in response.options:
   if opt.otype == dns.edns.RRSERIAL:
      print("Serial of the answer is %s" % struct.unpack(">I", opt.data)[0])

So we just have to convert the binary value into a string (the >I means a big-endian integer). The complete Python code is in test-rrserial.py.

In Go, we will use the godns library. Creating the option and adding it to the DNS query (m in the code) is done as follows:

m.Question = make([]dns.Question, 1)
// Every EDNS pseudo-record has the name "." (the root)
o.Hdr.Name = "."
o.Hdr.Rrtype = dns.TypeOPT
o.SetUDPSize(4096)
// Generic EDNS option
e := new(dns.EDNS0_LOCAL)
e.Code = otype
// Empty request
e.Data = []byte{}
o.Option = append(o.Option, e)
// Extra is the Additional section
m.Extra = append(m.Extra, o)

and to read the result:

opt := msg.IsEdns0()
for _, v := range opt.Option {
    // Thanks to Tom Thorogood for the reminder to force the guy
    // and therefore have a new variable v (the ': =').
    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)

The complete Go code is in test-rrserial.go.