Adding new RR types to GO DNS

November 30, 2012

programming

Inspired by NLnet Labs and PowerDNS, I figured I couldn’t stay behind, so here is how to add new RRs to Go DNS.

A small note before I delve into the details, I haven’t optimized Go DNS for adding new types, as this is a relative infrequent event. There are a few items that need to be added before Go DNS understands the new RR type.

  1. Adding the type itself (as a structure) and the four methods needed to implement the RR interface;
  2. Adding the type number and the text string belonging it;
  3. Parsing from text, i.e. when parsing zonefiles.

Lets take the new DANE (RFC6698) as an example. The record is called TLSA, and looks like:

_443._tcp.www.example.com. IN TLSA 0 0 1 d2abde240d7cd3ee6b4b28c 54df034b9

The rdata consists out of four fields: 3 numbers (0 0 1) all encoded as uint8s and a string.

Adding the type

For the type to be known by Go DNS, we need to tweak two files: types.go and msg.go. In the file types.go, at the top we need to add the code point for TLSA:

TypeTLSA       uint16 = 52

and at the bottom we need to add a new function in the rr_mk map:

TypeTLSA:       func() RR { return new(RR_TLSA) },

Next, in the file msg.go we add TLSA to the Rr_str map:

TypeTLSA:       "TSLA",

After this ground work we can add the actual definition of the record itself. The TLSA record consists out of a header (as all RR types do) and in this case four rdata fields, three uint8 values and a string encoded as hex. So we add the struct:

First we create the structure:

type RR_TLSA struct {
        Hdr          RR_Header
        Usage        uint8
        Selector     uint8
        MatchingType uint8
        Certificate  string `dns:"hex"`
}

The first four items are almost directly copied from the RFC, the tag dns:"hex" tells Go DNS that Certificate is a string which should be encoded as Hexadecimal.

And then the four methods needed to make the RR_TLSA structure implement the RR interface: Header() – returns the header of the RR, Copy() – needed for DNSSEC when parts of the RR need to be lowercased, Len() – returns the length of the RR, and String() – returns the string representation of the record:

func (rr *RR_TLSA) Header() *RR_Header { return &rr.Hdr }
func (rr *RR_TLSA) Len() int           { return rr.Hdr.Len() + 3 + len(rr.Certificate)/2 }
func (rr *RR_TLSA) Copy() RR {
return &RR_TLSA{*rr.Hdr.CopyHeader(), rr.Usage, rr.Selector, rr.MatchingType, rr.Certificate}
}

A finally String():

func (rr *RR_TLSA) String() string {
    return rr.Hdr.String() +
            " " + strconv.Itoa(int(rr.Usage)) +
            " " + strconv.Itoa(int(rr.Selector)) +
            " " + strconv.Itoa(int(rr.MatchingType)) +
            " " + rr.Certificate
}

After this, Go DNS knowns about the record, but it is not able to parse it from a string.

Parsing

Adding parsing support is done in the file zscan_rr.go, it entails of adding a whole function that actually parses the rdata. In the main switch in the function setRR, there are two places where are new function call (setTLSA) can be added. The ones following with goto Slurp have a fixed ending, the ones without goto Slurp have a variable ending. The TLSA record has be variable ending because the certificate length is not explicitly encoded in the RR. The MX record, for instance, has a fixed ending, because the length of the domain name is encoded in the record.

So we need to add the TLSA parsing in the bottom half:

case TypeTLSA:
    return setTLSA(h, c, f)

Some setXX functions also have an o parameter, which is the current origin. It is used for resource records which have unqualified domain names in the rdata, to which that origin should be concatenated.

Finally we write the setTLSA function, which looks like:

func setTLSA(h RR_Header, c chan lex, f string) (RR, *ParseError) {
        rr := new(RR_TLSA)
        rr.Hdr = h
        l := <-c
        if i, e := strconv.Atoi(l.token); e != nil {
                return nil, &ParseError{f, "bad TLSA Usage", l}
        } else {
                rr.Usage = uint8(i)
        }
        <-c // _BLANK
        l = <-c
        if i, e := strconv.Atoi(l.token); e != nil {
                return nil, &ParseError{f, "bad TLSA Selector", l}
        } else {
                rr.Selector = uint8(i)
        }
        <-c // _BLANK
        l = <-c
        if i, e := strconv.Atoi(l.token); e != nil {
                return nil, &ParseError{f, "bad TLSA MatchingType", l}
        } else {
                rr.MatchingType = uint8(i)
        }
        s, e := endingToString(c, "bad TLSA Certificate", f)
        if e != nil {
                return nil, e
        }
        rr.Certificate = s
        return rr, nil
}

The channel c returns normalized tokens, i.e. all tokens are separated by only one space and the resource record is ended when a _NEWLINE token is seen. So it’s a matter of picking of the tokens and converting them to the correct type(s) for the record.

Golang