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 }