IDN and Private RR in Go DNS

September 21, 2014

dns

Thanks to the excellent work from Alex Sergeyev, Go DNS has gotten some new features. I want to highlight two: IDN (https://www.ietf.org/rfc/rfc3492.txt) and Private RR support (http://tools.ietf.org/html/rfc6895).

IDN

This adds support for converting from and to Punycode. There is no explicit support, you will need to call idn.ToPunycode and idn.FromPunyCode yourself if you are dealing with IDNs.

The examples give in the code:

name := "インターネット.テスト"
fmt.Printf("%s -> %s", name, idn.ToPunycode(name))

Which outputs:

インターネット.テスト -> xn--eckucmux0ukc.xn--zckzah

Private RR

Another thing that was added is the ability to add private RR types: that is new RR type with a code in the 65,280 - 65,534 range. This makes it possible to specify your own (vanity) types, which then work just as the normal ones in Go DNS.

Say for instance I want to add a new RR called “MIEK” (with type code 65281), this RR has two rdata elements: a domain name and it ends with a hexadecimal string. Basically it looks like:

www.example.com.    IN MIEK     example.com. AABB1234

Note that you can also use TYPExxxx and RFC3597 (http://www.ietf.org/rfc/rfc3597.txt) for this, but then the above RR would look something like this:

www.example.com.    IN TYPE65281 \# 8 AABBCCDDAABB1234

Which works, but of course looks bad in the vanity department.

To do this you will need (see example_test.go for example code in the library) create a new record type, implement the PrivateRdata interface and register the new type with the library.

const typeMiek = 65281
type MIEK struct {
    Target string
    Extra  string
}

Implement PrivateRdata:

  1. String - returns the string presentation of the rdata;
  2. Len - returns the length of the rdata;
  3. Pack and Unpack to convert to and from wire format;
  4. Parse to parse a text presentation of the rdata;
  5. Copy to allow the private RR to be copied inside Go DNS (sometimes needed for DNSSEC).

So lets start with the implementation beginning with String() and Len()

func (m *MIEK) String() string { return m.Target + " " + m.Extra }
func (m *MIEK) Len() int  { return len(m.Target) + len(m.Extra)/2 }

And the functions to convert from and to wire format:

func (m *MIEK) Pack(buf []byte) (int, error) {
        off, err := dns.PackDomainName(m.Target, buf, 0, nil, false)
        if err != nil {
                return off, err
        }
        h, err := hex.DecodeString(m.Extra)
        if err != nil {
                return off, err
        }
        if off+hex.DecodedLen(len(m.Extra)) > len(buf) {
                return len(buf), errors.New("overflow packing hex")
        }
        copy(buf[off:off+hex.DecodedLen(len(m.Extra))], h)
        off += hex.DecodedLen(len(m.Extra))
        return off, nil
}

func (m *MIEK) Unpack(buf []byte) (int, error) {
        s, off, err := dns.UnpackDomainName(buf, 0)
        if err != nil {
                return len(buf), err
        }
        m.Target = s
        s = hex.EncodeToString(buf[off:])
        m.Extra = s
        return len(buf), nil
}

And our Parse function that parses the string slice sx into MIEK’s rdata.

func (m *MIEK) Parse(sx []string) error {
        if len(sx) < 2 {
                return errors.New("need at least 2 pieces of rdata")
        }
        m.Target = sx[0]
        if  _, ok := dns.IsDomainName(m.Target); !ok {
                return errors.New("bad MIEK Target")
        }
        // Hex data can contain spaces.
        for _, s := range sx[1:] {
                m.Extra += s
        }
        return nil
}

And Copy which is needed to copy the rdata:

func (m *MIEK) Copy(dest dns.PrivateRdata) error {
        m1, ok := dest.(*MIEK)
        if !ok {
                return dns.ErrRdata
        }
        m1.Target = m.Target
        m1.Extra = m.Extra
        return nil
}

And finally we can register the new type:

dns.PrivateHandle("MIEK", typeMiek, func() dns.PrivateRdata { return new(MIEK) })
defer dns.PrivateHandleRemove(typeMiek) // when removing again

We can now use all this code to do the following:

m1, _ := dns.NewRR("example.com. IN MIEK target.example.com. DEADBEEF")
log.Printf("%s\n", m1.String())

That prints:

 example.com.   3600    IN  MIEK    target.example.com. DEADBEEF

Mission accomplished :) If you need to export this new type, your best bet is to convert it to an unknown RR:

u := new(dns.RFC3597)
u.ToRFC3597(m1)
log.Printf("%s\n", u.String())

Yields: example.com. 3600 IN MIEK \# 24 06746172676574076578616d706c6503636f6d00deadbeef

Private RR  IDN  DNS