tessl install tessl/golang-github-com-miekg--dns@1.1.1Complete DNS library for Go with full protocol control, DNSSEC support, and both client and server programming capabilities
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
})