AXFR (full zone transfer) and IXFR (incremental zone transfer) client and server support.
Zone transfer client for receiving zones from servers.
type Transfer struct {
*Conn // Embedded connection
DialTimeout time.Duration // Dial timeout (default: 2 seconds)
ReadTimeout time.Duration // Read timeout (default: 2 seconds)
WriteTimeout time.Duration // Write timeout (default: 2 seconds)
TsigProvider TsigProvider // Custom TSIG implementation
TsigSecret map[string]string // TSIG secrets: map[zonename]base64secret
TLS *tls.Config // TLS configuration for XFR over TLS
// contains filtered or unexported fields
}
// In performs incoming zone transfer
// Returns channel of Envelopes containing RRs
func (t *Transfer) In(q *Msg, a string) (chan *Envelope, error)
// Out performs outgoing zone transfer
// Used by servers to send zone data
func (t *Transfer) Out(w ResponseWriter, q *Msg, ch chan *Envelope) error
// ReadMsg reads message from transfer connection
func (t *Transfer) ReadMsg() (*Msg, error)
// WriteMsg writes message to transfer connection
func (t *Transfer) WriteMsg(m *Msg) errorContainer for zone transfer data and errors.
type Envelope struct {
RR []RR // Resource records from transfer message
Error error // Error if something went wrong
}// Create transfer request
m := new(dns.Msg)
m.SetAxfr("example.com.")
// Perform transfer
t := new(dns.Transfer)
c, err := t.In(m, "ns1.example.com:53")
if err != nil {
log.Fatal(err)
}
// Receive zone records
var zone []dns.RR
for env := range c {
if env.Error != nil {
log.Printf("Error: %v", env.Error)
break
}
zone = append(zone, env.RR...)
}
fmt.Printf("Received %d records\n", len(zone))// Get current SOA serial
soa := &dns.SOA{
Hdr: dns.RR_Header{
Name: "example.com.",
Rrtype: dns.TypeSOA,
Class: dns.ClassINET,
},
Serial: 2024010101, // Your current serial
}
// Create IXFR request
m := new(dns.Msg)
m.SetIxfr("example.com.", 2024010101, "ns1.example.com.", "admin.example.com.")
// Perform transfer
t := new(dns.Transfer)
c, err := t.In(m, "ns1.example.com:53")
if err != nil {
log.Fatal(err)
}
// Process incremental updates
for env := range c {
if env.Error != nil {
log.Printf("Error: %v", env.Error)
break
}
// IXFR returns SOA records to mark sections
// Check if it's a full transfer or incremental
for _, rr := range env.RR {
fmt.Println(rr.String())
}
}t := &dns.Transfer{
TsigSecret: map[string]string{
"example.com.": "base64secret==",
},
}
m := new(dns.Msg)
m.SetAxfr("example.com.")
m.SetTsig("example.com.", dns.HmacSHA256, 300, time.Now().Unix())
c, err := t.In(m, "ns1.example.com:53")
if err != nil {
log.Fatal(err)
}
for env := range c {
if env.Error != nil {
log.Fatal(env.Error)
}
// Process records
}tlsConfig := &tls.Config{
ServerName: "ns1.example.com",
}
t := &dns.Transfer{
TLS: tlsConfig,
}
m := new(dns.Msg)
m.SetAxfr("example.com.")
c, err := t.In(m, "ns1.example.com:853")
if err != nil {
log.Fatal(err)
}
for env := range c {
if env.Error != nil {
log.Fatal(env.Error)
}
// Process records
}// Set up custom dialer
dialer := &net.Dialer{
LocalAddr: &net.TCPAddr{
IP: net.ParseIP("192.0.2.100"),
},
Timeout: 5 * time.Second,
}
// Create connection
conn, err := dialer.Dial("tcp", "ns1.example.com:53")
if err != nil {
log.Fatal(err)
}
// Use connection with Transfer
dnsConn := &dns.Conn{Conn: conn}
t := &dns.Transfer{Conn: dnsConn}
m := new(dns.Msg)
m.SetAxfr("example.com.")
c, err := t.In(m, "ns1.example.com:53")
if err != nil {
log.Fatal(err)
}
for env := range c {
if env.Error != nil {
log.Fatal(env.Error)
}
// Process records
}func DownloadZone(zone, server string) ([]dns.RR, error) {
m := new(dns.Msg)
m.SetAxfr(dns.Fqdn(zone))
t := &dns.Transfer{
DialTimeout: 10 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
c, err := t.In(m, server)
if err != nil {
return nil, err
}
var records []dns.RR
for env := range c {
if env.Error != nil {
return nil, env.Error
}
records = append(records, env.RR...)
}
return records, nil
}
// Usage
zone, err := DownloadZone("example.com", "ns1.example.com:53")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Downloaded %d records\n", len(zone))func ProcessIXFR(zone string, currentSerial uint32, server string) error {
m := new(dns.Msg)
m.SetIxfr(zone, currentSerial, "ns1."+zone, "admin."+zone)
t := new(dns.Transfer)
c, err := t.In(m, server)
if err != nil {
return err
}
inDelete := false
for env := range c {
if env.Error != nil {
return env.Error
}
for _, rr := range env.RR {
if soa, ok := rr.(*dns.SOA); ok {
if soa.Serial == currentSerial {
inDelete = !inDelete
continue
}
if soa.Serial > currentSerial {
// New SOA
currentSerial = soa.Serial
fmt.Printf("Updated to serial: %d\n", currentSerial)
continue
}
}
if inDelete {
fmt.Printf("Delete: %s\n", rr.String())
} else {
fmt.Printf("Add: %s\n", rr.String())
}
}
}
return nil
}dns.HandleFunc("example.com.", func(w dns.ResponseWriter, r *dns.Msg) {
if r.Question[0].Qtype != dns.TypeAXFR {
// Handle normal query
return
}
// Check authorization (e.g., TSIG, IP address)
if r.IsTsig() == nil {
m := new(dns.Msg)
m.SetRcode(r, dns.RcodeRefused)
w.WriteMsg(m)
return
}
// Load zone records
zone := loadZoneRecords("example.com.")
// Send zone via channel
ch := make(chan *dns.Envelope)
tr := new(dns.Transfer)
go func() {
// SOA must be first and last
soa := zone[0]
// Send in chunks
chunk := make([]dns.RR, 0, 100)
chunk = append(chunk, soa)
for _, rr := range zone[1:] {
chunk = append(chunk, rr)
if len(chunk) >= 100 {
ch <- &dns.Envelope{RR: chunk}
chunk = make([]dns.RR, 0, 100)
}
}
// Send remaining records with final SOA
if len(chunk) > 0 {
chunk = append(chunk, soa)
ch <- &dns.Envelope{RR: chunk}
} else {
ch <- &dns.Envelope{RR: []dns.RR{soa}}
}
close(ch)
}()
if err := tr.Out(w, r, ch); err != nil {
log.Printf("Transfer error: %v", err)
}
})dns.HandleFunc("example.com.", func(w dns.ResponseWriter, r *dns.Msg) {
if r.Question[0].Qtype != dns.TypeAXFR {
return
}
// Verify TSIG
if tsig := r.IsTsig(); tsig != nil {
if w.TsigStatus() != nil {
m := new(dns.Msg)
m.SetRcode(r, dns.RcodeNotAuth)
w.WriteMsg(m)
return
}
} else {
// No TSIG, refuse
m := new(dns.Msg)
m.SetRcode(r, dns.RcodeRefused)
w.WriteMsg(m)
return
}
// Perform transfer
zone := loadZoneRecords("example.com.")
ch := make(chan *dns.Envelope)
tr := new(dns.Transfer)
go func() {
ch <- &dns.Envelope{RR: zone}
close(ch)
}()
tr.Out(w, r, ch)
})
// Server with TSIG secret
server := &dns.Server{
Addr: ":53",
Net: "tcp",
TsigSecret: map[string]string{
"example.com.": "base64secret==",
},
}
server.ListenAndServe()dns.HandleFunc("example.com.", func(w dns.ResponseWriter, r *dns.Msg) {
if r.Question[0].Qtype != dns.TypeIXFR {
return
}
// Get client's current serial from Authority section
if len(r.Ns) == 0 {
m := new(dns.Msg)
m.SetRcode(r, dns.RcodeFormatError)
w.WriteMsg(m)
return
}
clientSOA, ok := r.Ns[0].(*dns.SOA)
if !ok {
m := new(dns.Msg)
m.SetRcode(r, dns.RcodeFormatError)
w.WriteMsg(m)
return
}
clientSerial := clientSOA.Serial
currentZone := loadZoneRecords("example.com.")
currentSOA := currentZone[0].(*dns.SOA)
ch := make(chan *dns.Envelope)
tr := new(dns.Transfer)
go func() {
if clientSerial >= currentSOA.Serial {
// No changes - send current SOA only
ch <- &dns.Envelope{RR: []dns.RR{currentSOA}}
} else if hasIncrementalData(clientSerial) {
// Send IXFR
sendIXFR(ch, clientSerial, currentSOA)
} else {
// Fall back to AXFR
sendAXFR(ch, currentZone)
}
close(ch)
}()
tr.Out(w, r, ch)
})func serveAXFR(w dns.ResponseWriter, r *dns.Msg) {
zone := "example.com."
// Open zone file
file, err := os.Open("/var/named/zones/db." + zone)
if err != nil {
m := new(dns.Msg)
m.SetRcode(r, dns.RcodeServerFailure)
w.WriteMsg(m)
return
}
defer file.Close()
// Parse zone incrementally
zp := dns.NewZoneParser(file, zone, file.Name())
ch := make(chan *dns.Envelope)
tr := new(dns.Transfer)
go func() {
chunk := make([]dns.RR, 0, 100)
var soa dns.RR
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
if soa == nil {
if _, ok := rr.(*dns.SOA); ok {
soa = rr
chunk = append(chunk, soa)
}
continue
}
chunk = append(chunk, rr)
if len(chunk) >= 100 {
ch <- &dns.Envelope{RR: chunk}
chunk = make([]dns.RR, 0, 100)
}
}
// Send remaining + final SOA
if len(chunk) > 0 {
chunk = append(chunk, soa)
ch <- &dns.Envelope{RR: chunk}
}
if err := zp.Err(); err != nil {
ch <- &dns.Envelope{Error: err}
}
close(ch)
}()
tr.Out(w, r, ch)
}func SyncSecondary(zone, primary string) error {
// Get current SOA
currentSOA := getCurrentSOA(zone)
// Try IXFR first
m := new(dns.Msg)
if currentSOA != nil {
m.SetIxfr(zone, currentSOA.Serial, "ns1."+zone, "admin."+zone)
} else {
m.SetAxfr(zone)
}
t := &dns.Transfer{
DialTimeout: 30 * time.Second,
ReadTimeout: 30 * time.Second,
}
c, err := t.In(m, primary)
if err != nil {
return err
}
// Process transfer
return applyTransfer(zone, c)
}func DownloadZones(zones []string, server string) error {
var wg sync.WaitGroup
errChan := make(chan error, len(zones))
for _, zone := range zones {
wg.Add(1)
go func(z string) {
defer wg.Done()
records, err := DownloadZone(z, server)
if err != nil {
errChan <- fmt.Errorf("zone %s: %w", z, err)
return
}
// Save zone
if err := saveZone(z, records); err != nil {
errChan <- err
}
}(zone)
}
wg.Wait()
close(errChan)
// Check for errors
for err := range errChan {
if err != nil {
return err
}
}
return nil
}1. Client sends AXFR query
2. Server responds with messages containing:
- First message: SOA (start of zone)
- Middle messages: All other RRs
- Last message: SOA (end of zone)1. Client sends IXFR query with current SOA in Authority section
2. Server responds with either:
a) AXFR (if incremental data unavailable)
b) IXFR differential:
- Current SOA
- For each change set:
* SOA (serial N-1) - marks deletions
* Records to delete
* SOA (serial N) - marks additions
* Records to add
- Final current SOAvar (
ErrSoa error // SOA record error
ErrId error // Message ID mismatch
)for env := range c {
if env.Error != nil {
switch env.Error {
case dns.ErrSoa:
log.Println("Invalid SOA in zone transfer")
case dns.ErrId:
log.Println("Message ID mismatch")
default:
log.Printf("Transfer error: %v", env.Error)
}
break
}
// Process env.RR
}// IP-based access control
remoteIP := getRemoteIP(w)
if !isAllowed(remoteIP) {
m := new(dns.Msg)
m.SetRcode(r, dns.RcodeRefused)
w.WriteMsg(m)
return
}
// TSIG-based authentication (recommended)
if r.IsTsig() == nil || w.TsigStatus() != nil {
m := new(dns.Msg)
m.SetRcode(r, dns.RcodeNotAuth)
w.WriteMsg(m)
return
}var xfrLimiter = rate.NewLimiter(rate.Every(1*time.Minute), 5)
dns.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) {
if r.Question[0].Qtype == dns.TypeAXFR || r.Question[0].Qtype == dns.TypeIXFR {
if !xfrLimiter.Allow() {
m := new(dns.Msg)
m.SetRcode(r, dns.RcodeRefused)
w.WriteMsg(m)
return
}
}
// Handle transfer
})