112 lines
2.5 KiB
Go
112 lines
2.5 KiB
Go
package godns
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// TransferResult is a completed AXFR.
|
|
// Serial is the SOA serial and is the same value stored on Zone.
|
|
type TransferResult struct {
|
|
Zone Zone
|
|
Serial uint32
|
|
}
|
|
|
|
// AXFR reads the full zone over TCP, signed with the session TSIG key.
|
|
// It returns an error when ctx is done, the transfer fails, a record owner
|
|
// is not ASCII, or the stream contains no SOA.
|
|
// The closing SOA of the transfer is not appended again.
|
|
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
|
|
}
|
|
|
|
// tsigAlgorithm maps a BIND algorithm name to miekg/dns constants.
|
|
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)
|
|
}
|
|
}
|
|
|
|
// timeoutFrom uses the context deadline, or 30s, with a 1s floor.
|
|
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
|
|
}
|