From b2701161cb6d7e11f5d5c96915fa7ae2f883d1e6 Mon Sep 17 00:00:00 2001 From: Simon Zeyer Date: Tue, 24 Feb 2026 19:50:54 +0000 Subject: [PATCH] add AXFR request handling to support slaves --- pkg/acmedns/interfaces.go | 5 ++ pkg/database/db.go | 44 +++++++++++++++ pkg/nameserver/handler.go | 110 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+) diff --git a/pkg/acmedns/interfaces.go b/pkg/acmedns/interfaces.go index 2734af2..7fa9880 100644 --- a/pkg/acmedns/interfaces.go +++ b/pkg/acmedns/interfaces.go @@ -10,6 +10,7 @@ type AcmednsDB interface { Register(cidrslice Cidrslice) (ACMETxt, error) GetByUsername(uuid.UUID) (ACMETxt, error) GetTXTForDomain(string) ([]string, error) + GetTXTForAllDomains() ([]TXTRecord, error) Update(ACMETxtPost) error GetBackend() *sql.DB SetBackend(*sql.DB) @@ -23,4 +24,8 @@ type AcmednsNS interface { ParseRecords() BumpSerial() error } + +type TXTRecord struct { + Subdomain string + Value string } diff --git a/pkg/database/db.go b/pkg/database/db.go index 00f60ef..2b0688d 100644 --- a/pkg/database/db.go +++ b/pkg/database/db.go @@ -263,6 +263,50 @@ func (d *acmednsdb) GetByUsername(u uuid.UUID) (acmedns.ACMETxt, error) { return acmedns.ACMETxt{}, fmt.Errorf("user not found: %s", u.String()) } +func (d *acmednsdb) GetTXTForAllDomains() ([]acmedns.TXTRecord, error) { + d.Mutex.Lock() + defer d.Mutex.Unlock() + + var txts []acmedns.TXTRecord + + getSQL := ` + SELECT Subdomain, Value FROM txt + ` + if d.Config.Database.Engine == "sqlite" { + getSQL = getSQLiteStmt(getSQL) + } + + sm, err := d.DB.Prepare(getSQL) + if err != nil { + return txts, err + } + defer sm.Close() + + rows, err := sm.Query() + if err != nil { + return txts, err + } + defer rows.Close() + + for rows.Next() { + var subdomain string + var value string + + err = rows.Scan(&subdomain, &value) + if err != nil { + return txts, err + } + + d.Logger.Debugw("GetTXTForAllDomains() TXT Record:", subdomain, value) + txts = append(txts, acmedns.TXTRecord{ + Subdomain: subdomain, + Value: value, + }) + } + + return txts, nil +} + func (d *acmednsdb) GetTXTForDomain(domain string) ([]string, error) { d.Mutex.Lock() defer d.Mutex.Unlock() diff --git a/pkg/nameserver/handler.go b/pkg/nameserver/handler.go index e8a8b14..204390f 100644 --- a/pkg/nameserver/handler.go +++ b/pkg/nameserver/handler.go @@ -2,12 +2,48 @@ package nameserver import ( "fmt" + "net" "strings" "github.com/miekg/dns" ) func (n *Nameserver) handleRequest(w dns.ResponseWriter, r *dns.Msg) { + + if len(r.Question) == 1 { + q := r.Question[0] + if q.Qtype == dns.TypeAXFR || q.Qtype == dns.TypeIXFR { // Get remote IP + remoteIP, _, err := net.SplitHostPort(w.RemoteAddr().String()) + if err != nil { + n.Logger.Errorw("Failed to parse remote address", "err", err) + m := new(dns.Msg) + m.SetReply(r) + m.Rcode = dns.RcodeRefused + _ = w.WriteMsg(m) + return + } + + // Check if remote IP is in slave list + allowed := false + for _, slave := range n.Config.General.SlaveHosts { + if remoteIP == slave { + allowed = true + break + } + } + + if !allowed { + n.Logger.Warnw("AXFR/IXFR request denied", "remote", remoteIP) + m := new(dns.Msg) + m.SetReply(r) + m.Rcode = dns.RcodeRefused + _ = w.WriteMsg(m) + return + } + n.handleAXFR(w, r) + return + } + } m := new(dns.Msg) m.SetReply(r) // handle edns0 @@ -161,3 +197,77 @@ func (n *Nameserver) getRecord(name string, qtype uint16) ([]dns.RR, error) { } return rr, nil } + +func (n *Nameserver) handleAXFR(w dns.ResponseWriter, r *dns.Msg) { + + if len(r.Question) == 0 { + return + } + + zone := dns.Fqdn(r.Question[0].Name) + + records, ok := n.Domains[zone] + if !ok { + m := new(dns.Msg) + m.SetRcode(r, dns.RcodeNameError) + _ = w.WriteMsg(m) + return + } + + // AXFR muss über Transfer laufen + tr := new(dns.Transfer) + + c := make(chan *dns.Envelope) + + go func() { + defer close(c) + + var rr []dns.RR + + // Start SOA + rr = append(rr, n.SOA) + + // NS + rr = append(rr, records.NS...) + + // Andere Records + // rr = append(rr, filterSOA(records.Records)...) + rr = append(rr, records.Records...) + + // TXT Records nur für diese Zone! + txtRecords, err := n.DB.GetTXTForAllDomains() + if err == nil { + for _, rec := range txtRecords { + if rec.Value == "" { + continue + } + + fqdn := dns.Fqdn(rec.Subdomain + "." + zone) + + txtRR := &dns.TXT{ + Hdr: dns.RR_Header{ + Name: fqdn, + Rrtype: dns.TypeTXT, + Class: dns.ClassINET, + Ttl: 1, + }, + Txt: []string{rec.Value}, + } + + rr = append(rr, txtRR) + + n.Logger.Debugw("handleAXFR TXT Record", "subdomain", rec.Subdomain, "value", rec.Value, "fqdn", fqdn) + rr = append(rr, txtRR) + } + } else { + n.Logger.Errorw("Failed to get TXT records for AXFR", "error", err) + } + + // End SOA + rr = append(rr, n.SOA) + + c <- &dns.Envelope{RR: rr} + }() + + _ = tr.Out(w, r, c) +}