Adding new RR types to GO DNS
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.
- Adding the type itself (as a structure) and the four methods needed to implement the
RR
interface; - Adding the type number and the text string belonging it;
- 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 uint8
s 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 ano
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.