or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

client.mdconstants.mddnssec.mddynamic-updates.mdedns0.mdindex.mdmessaging.mdrr-types.mdserver.mdtsig.mdutilities.mdzone-parsing.mdzone-transfer.md
tile.json

zone-transfer.mddocs/

Zone Transfers

AXFR (full zone transfer) and IXFR (incremental zone transfer) client and server support.

Core Types

Transfer

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) error

Envelope

Container for zone transfer data and errors.

type Envelope struct {
	RR    []RR  // Resource records from transfer message
	Error error // Error if something went wrong
}

Client Operations (Incoming Transfers)

AXFR - Full Zone Transfer

// 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))

IXFR - Incremental Zone Transfer

// 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())
	}
}

AXFR with TSIG Authentication

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
}

XFR over TLS

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
}

Using Custom Connection

// 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
}

Complete AXFR Client Example

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))

Processing IXFR Differentials

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
}

Server Operations (Outgoing Transfers)

Basic AXFR Server

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)
	}
})

AXFR Server with TSIG

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()

IXFR Server

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)
})

Streaming Large Zone

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)
}

Usage Patterns

Synchronizing Secondary Server

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)
}

Parallel Zone Downloads

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
}

Zone Transfer Message Format

AXFR Message Sequence

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)

IXFR Message Sequence

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 SOA

Error Handling

var (
	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
}

Security Considerations

Access Control

// 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
}

Rate Limiting

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
})

Performance Tips

  1. Chunking: Send records in reasonable chunks (100-500 records)
  2. Streaming: Parse and send zones incrementally for large zones
  3. Compression: Enable DNS name compression in responses
  4. TCP Tuning: Adjust TCP buffer sizes for large transfers
  5. Timeouts: Set appropriate timeouts for large zones

Related Topics

  • DNS Messaging - Message construction for transfers
  • TSIG Authentication - Securing zone transfers
  • Zone Parsing - Parsing zone files
  • Server Operations - DNS server setup
  • Client Operations - DNS client usage