package godns import ( "fmt" "net" "strings" "github.com/miekg/dns" ) // Record is a single resource record with presentation-format rdata. 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 a complete in-memory zone from AXFR (or assembled records). type Zone struct { Origin string Serial uint32 Records []Record } // RRSets groups Zone records by owner/class/type, preserving 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 IPv4 address record. 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 IPv6 address record. 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 a mail-exchanger record. 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 a text record. 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 a canonical-name record. 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 a nameserver record. 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 a service locator record. 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 a pointer record. 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}) } 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} } 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 } 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 } func fqdnASCII(name string) (string, error) { if err := asciiName("name", name); err != nil { return "", err } return dns.Fqdn(strings.TrimSpace(name)), nil } func isASCII(s string) bool { for i := 0; i < len(s); i++ { if s[i] > 127 { return false } } return true }