209 lines
6.3 KiB
Go
209 lines
6.3 KiB
Go
package godns
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// Record is one resource record.
|
|
// Owner is a trailing-dot FQDN. Class and Type are presentation names such as IN and A.
|
|
// RData is the presentation-format rdata, without the owner, TTL, class, or type.
|
|
type Record struct {
|
|
Owner string
|
|
Class string
|
|
Type string
|
|
TTL uint32
|
|
RData string
|
|
}
|
|
|
|
// RRSet groups records that share owner, class, and type.
|
|
type RRSet struct {
|
|
Owner string
|
|
Class string
|
|
Type string
|
|
Records []Record
|
|
}
|
|
|
|
// Zone is an in-memory zone: origin, SOA serial, and a flat record list.
|
|
// AXFR returns one. Callers can also assemble a Zone and call RRSets.
|
|
type Zone struct {
|
|
Origin string
|
|
Serial uint32
|
|
Records []Record
|
|
}
|
|
|
|
// RRSets groups Zone records by owner, class, and type, in first-seen order.
|
|
func (z Zone) RRSets() []RRSet {
|
|
type key struct{ owner, class, typ string }
|
|
order := make([]key, 0)
|
|
grouped := make(map[key][]Record)
|
|
for _, rec := range z.Records {
|
|
k := key{rec.Owner, rec.Class, rec.Type}
|
|
if _, ok := grouped[k]; !ok {
|
|
order = append(order, k)
|
|
}
|
|
grouped[k] = append(grouped[k], rec)
|
|
}
|
|
out := make([]RRSet, 0, len(order))
|
|
for _, k := range order {
|
|
out = append(out, RRSet{Owner: k.owner, Class: k.class, Type: k.typ, Records: grouped[k]})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// A builds an IN A record.
|
|
// It returns an error when ip is not IPv4, or when owner is empty or not ASCII.
|
|
// Owner is stored as a trailing-dot FQDN.
|
|
func A(owner string, ttl uint32, ip net.IP) (Record, error) {
|
|
v4 := ip.To4()
|
|
if v4 == nil {
|
|
return Record{}, fmt.Errorf("A record requires an IPv4 address")
|
|
}
|
|
return fromRR(&dns.A{Hdr: rrHeader(owner, dns.TypeA, ttl), A: v4})
|
|
}
|
|
|
|
// AAAA builds an IN AAAA record.
|
|
// It returns an error when ip is missing, is IPv4, or when owner is empty or not ASCII.
|
|
// Owner is stored as a trailing-dot FQDN.
|
|
func AAAA(owner string, ttl uint32, ip net.IP) (Record, error) {
|
|
if ip == nil || ip.To16() == nil || ip.To4() != nil {
|
|
return Record{}, fmt.Errorf("AAAA record requires an IPv6 address")
|
|
}
|
|
return fromRR(&dns.AAAA{Hdr: rrHeader(owner, dns.TypeAAAA, ttl), AAAA: ip.To16()})
|
|
}
|
|
|
|
// MX builds an IN MX record for preference pref and exchanger target.
|
|
// It returns an error when owner or target is empty or not ASCII.
|
|
func MX(owner string, ttl uint32, pref uint16, target string) (Record, error) {
|
|
name, err := fqdnASCII(target)
|
|
if err != nil {
|
|
return Record{}, err
|
|
}
|
|
return fromRR(&dns.MX{Hdr: rrHeader(owner, dns.TypeMX, ttl), Preference: pref, Mx: name})
|
|
}
|
|
|
|
// TXT builds an IN TXT record whose rdata is text.
|
|
// It returns an error when owner is empty or not ASCII.
|
|
func TXT(owner string, ttl uint32, text string) (Record, error) {
|
|
return fromRR(&dns.TXT{Hdr: rrHeader(owner, dns.TypeTXT, ttl), Txt: []string{text}})
|
|
}
|
|
|
|
// CNAME builds an IN CNAME record pointing at target.
|
|
// It returns an error when owner or target is empty or not ASCII.
|
|
func CNAME(owner string, ttl uint32, target string) (Record, error) {
|
|
name, err := fqdnASCII(target)
|
|
if err != nil {
|
|
return Record{}, err
|
|
}
|
|
return fromRR(&dns.CNAME{Hdr: rrHeader(owner, dns.TypeCNAME, ttl), Target: name})
|
|
}
|
|
|
|
// NS builds an IN NS record pointing at target.
|
|
// It returns an error when owner or target is empty or not ASCII.
|
|
func NS(owner string, ttl uint32, target string) (Record, error) {
|
|
name, err := fqdnASCII(target)
|
|
if err != nil {
|
|
return Record{}, err
|
|
}
|
|
return fromRR(&dns.NS{Hdr: rrHeader(owner, dns.TypeNS, ttl), Ns: name})
|
|
}
|
|
|
|
// SRV builds an IN SRV record.
|
|
// It returns an error when owner or target is empty or not ASCII.
|
|
func SRV(owner string, ttl uint32, prio, weight, port uint16, target string) (Record, error) {
|
|
name, err := fqdnASCII(target)
|
|
if err != nil {
|
|
return Record{}, err
|
|
}
|
|
return fromRR(&dns.SRV{
|
|
Hdr: rrHeader(owner, dns.TypeSRV, ttl),
|
|
Priority: prio,
|
|
Weight: weight,
|
|
Port: port,
|
|
Target: name,
|
|
})
|
|
}
|
|
|
|
// PTR builds an IN PTR record pointing at target.
|
|
// It returns an error when owner or target is empty or not ASCII.
|
|
func PTR(owner string, ttl uint32, target string) (Record, error) {
|
|
name, err := fqdnASCII(target)
|
|
if err != nil {
|
|
return Record{}, err
|
|
}
|
|
return fromRR(&dns.PTR{Hdr: rrHeader(owner, dns.TypePTR, ttl), Ptr: name})
|
|
}
|
|
|
|
// CAA builds an IN CAA record (RFC 8659) with flag, tag, and value.
|
|
// It returns an error when owner is empty or not ASCII.
|
|
func CAA(owner string, ttl uint32, flag uint8, tag, value string) (Record, error) {
|
|
return fromRR(&dns.CAA{
|
|
Hdr: rrHeader(owner, dns.TypeCAA, ttl),
|
|
Flag: flag,
|
|
Tag: tag,
|
|
Value: value,
|
|
})
|
|
}
|
|
|
|
// rrHeader builds an INET header. Owner ASCII is checked in fromRR.
|
|
func rrHeader(owner string, typ uint16, ttl uint32) dns.RR_Header {
|
|
name, err := fqdnASCII(owner)
|
|
if err != nil {
|
|
// fromRR is not called on this path; builders call fqdn via fromRR after Header is set.
|
|
// Validate in fromRR instead so the error is returned to the caller.
|
|
name = owner
|
|
}
|
|
return dns.RR_Header{Name: name, Rrtype: typ, Class: dns.ClassINET, Ttl: ttl}
|
|
}
|
|
|
|
// fromRR maps a miekg/dns RR onto the public Record type.
|
|
func fromRR(rr dns.RR) (Record, error) {
|
|
hdr := rr.Header()
|
|
if err := asciiName("owner", strings.TrimSuffix(hdr.Name, ".")); err != nil {
|
|
return Record{}, err
|
|
}
|
|
if hdr.Name == "" || !isASCII(hdr.Name) {
|
|
return Record{}, fmt.Errorf("owner must be ASCII, got %q", hdr.Name)
|
|
}
|
|
return Record{
|
|
Owner: dns.Fqdn(hdr.Name),
|
|
Class: dns.ClassToString[hdr.Class],
|
|
Type: dns.TypeToString[hdr.Rrtype],
|
|
TTL: hdr.Ttl,
|
|
RData: strings.TrimSpace(strings.TrimPrefix(rr.String(), hdr.String())),
|
|
}, nil
|
|
}
|
|
|
|
// recordToRR encodes a Record as a miekg/dns RR for UPDATE.
|
|
func recordToRR(rec Record) (dns.RR, error) {
|
|
if err := asciiName("owner", rec.Owner); err != nil {
|
|
return nil, err
|
|
}
|
|
rr, err := dns.NewRR(fmt.Sprintf("%s %d %s %s %s", dns.Fqdn(rec.Owner), rec.TTL, rec.Class, rec.Type, rec.RData))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode %s %s: %w", rec.Type, rec.Owner, err)
|
|
}
|
|
return rr, nil
|
|
}
|
|
|
|
// fqdnASCII validates ASCII and returns a trailing-dot FQDN.
|
|
func fqdnASCII(name string) (string, error) {
|
|
if err := asciiName("name", name); err != nil {
|
|
return "", err
|
|
}
|
|
return dns.Fqdn(strings.TrimSpace(name)), nil
|
|
}
|
|
|
|
// isASCII reports whether s contains only bytes <= 127.
|
|
func isASCII(s string) bool {
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] > 127 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|