Implement AXFR over TCP and RFC 2136 UPDATE with caller TSIG.
Callers get a session API, typed RR builders, and BIND testcontainer proof so CheckDNS can stay off the DNS wire. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user