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,105 @@
|
||||
package godns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// TransferResult is a completed AXFR plus SOA serial metadata.
|
||||
type TransferResult struct {
|
||||
Zone Zone
|
||||
Serial uint32
|
||||
}
|
||||
|
||||
// AXFR fetches the full zone over TCP, signed with the session TSIG key.
|
||||
func (c *Client) AXFR(ctx context.Context) (*TransferResult, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
zone := dns.Fqdn(c.zone)
|
||||
msg := new(dns.Msg)
|
||||
msg.SetAxfr(zone)
|
||||
msg.SetTsig(dns.Fqdn(c.tsig.Name), tsigAlgorithm(c.tsig.Algorithm), 300, time.Now().Unix())
|
||||
|
||||
tr := &dns.Transfer{
|
||||
TsigSecret: map[string]string{dns.Fqdn(c.tsig.Name): c.tsig.Secret},
|
||||
DialTimeout: timeoutFrom(ctx),
|
||||
ReadTimeout: timeoutFrom(ctx),
|
||||
}
|
||||
|
||||
envCh, err := tr.In(msg, c.server)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AXFR to %s: %w", c.server, err)
|
||||
}
|
||||
|
||||
var records []Record
|
||||
var serial uint32
|
||||
sawSOA := false
|
||||
for env := range envCh {
|
||||
if env.Error != nil {
|
||||
return nil, fmt.Errorf("AXFR stream: %w", env.Error)
|
||||
}
|
||||
for _, rr := range env.RR {
|
||||
if soa, ok := rr.(*dns.SOA); ok {
|
||||
if sawSOA {
|
||||
continue // trailing SOA ends the transfer
|
||||
}
|
||||
sawSOA = true
|
||||
serial = soa.Serial
|
||||
}
|
||||
rec, err := fromRR(rr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records = append(records, rec)
|
||||
}
|
||||
}
|
||||
if !sawSOA {
|
||||
return nil, fmt.Errorf("AXFR returned no SOA for %s", zone)
|
||||
}
|
||||
return &TransferResult{
|
||||
Zone: Zone{
|
||||
Origin: zone,
|
||||
Serial: serial,
|
||||
Records: records,
|
||||
},
|
||||
Serial: serial,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func tsigAlgorithm(name string) string {
|
||||
trimmed := strings.ToLower(strings.TrimSpace(name))
|
||||
trimmed = strings.TrimSuffix(trimmed, ".")
|
||||
switch trimmed {
|
||||
case "hmac-md5":
|
||||
return dns.HmacMD5
|
||||
case "hmac-sha1":
|
||||
return dns.HmacSHA1
|
||||
case "hmac-sha224":
|
||||
return dns.HmacSHA224
|
||||
case "hmac-sha256":
|
||||
return dns.HmacSHA256
|
||||
case "hmac-sha384":
|
||||
return dns.HmacSHA384
|
||||
case "hmac-sha512":
|
||||
return dns.HmacSHA512
|
||||
default:
|
||||
return dns.Fqdn(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
func timeoutFrom(ctx context.Context) time.Duration {
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
return 30 * time.Second
|
||||
}
|
||||
d := time.Until(deadline)
|
||||
if d < time.Second {
|
||||
return time.Second
|
||||
}
|
||||
return d
|
||||
}
|
||||
Reference in New Issue
Block a user